HiveBrain v1.2.0
Get Started
← Back to all entries
patternMinor

L system equations

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
systemequationsstackoverflow

Problem

This is my first program on my second try to learn haskell :)

import Data.Maybe (mapMaybe)
import Control.Exception (try, IOException)
import System.Environment (getArgs)

type Rule = (Char, String)
type Axiom = String
type LSystem = (Axiom, [Rule])

getRule :: Char -> [Rule] -> String
getRule ch [] = [ch]
getRule ch ((rulekey, rulevalue):rest)
        | ch == rulekey = rulevalue
        | otherwise = getRule ch rest

lSystemIterate :: Int -> LSystem -> String
lSystemIterate 0 (axiom, _) = axiom
lSystemIterate n (axiom, rules) =
        lSystemIterate (n-1)
        (concatMap (\ch -> getRule ch rules) axiom, rules)

splitRule :: String -> Maybe (Char, String)
splitRule (x:' ':rest) = Just (x,rest)
splitRule _ = Nothing

readLSystem :: String -> IO (Maybe LSystem)
readLSystem filename = do
        strorexc  return Nothing
                Right [] -> return Nothing
                Right str -> return (Just (ax, mapMaybe splitRule rules))
                             where (ax:rules) = lines str

iterateIOMaybe :: Int -> IO (Maybe LSystem) -> IO (Maybe String)
iterateIOMaybe n = fmap $ fmap (lSystemIterate n)

main = do
        args  do
                        maybelsystem  putStrLn $ lSystemIterate (read n) lsystem
                                Nothing -> putStrLn "File doesn't exist, or it is empty"
                _ -> putStrLn "Usage: progname iterations filename"


Program invocation is something like this: runhaskell lsystem.hs 3 dragon.ls assuming that it is saved as lsystem.hs.

It takes two arguments, first argument is number of iterations, second argument is a filename to read L-System from.

File structure is simple, first line is axiom, each line of the form 'x ys' treated as transformation rule. So, something like this:

FX

X X+YF+
Y -FX-Y


As output, program should return something like;

FX+YF++-FX-YF++-FX+YF+--FX-YF+


So, how does it look?

Solution

This is good! You have restricted imports, type aliases, type signatures on all top-level definitions (except main, which you should add but is at least obvious), total functions... All I've got are details.

splitRule should use the Rule type alias in its return. I.e., splitRule :: String -> Maybe Rule. I would probably also name that function “readRule,” split refers to an implementation detail; you are providing the function with what you know (or hope) is a single serialized rule to be parsed, not (say) dividing a rule off by splitting it into two.

I might also reconsider your error handling with readRule, right now your program silently discards rules it can't parse. That's certainly a valid choice but you might find that it actually makes sense to halt program execution on badly defined input, like so—

readRule :: String -> Rule
readRule (x:' ':rest) = (x, rest)
readRule bad          = error $ "readRule: Can't parse " ++ show bad


Just something to think about. Note also how I aligned both cases of the function on = so their right hand sides start in the same column. This can be tedious but aligning things like multiline functions or lists can really help readers of your code as they scan through, the extra whitespace and visual similarity helps stop your Haskell code from looking like word soup.

You could do with another few type aliases as well to clear up the purposes of Chars and Strings in your program. At the very least I'd give—

type Predecessor = Char
type Successor   = String
type Rule        = (Predecessor, Successor)


I think the type of getRule is more obvious then.

getRule :: Predecessor -> [Rule] -> Successor


It also shows that getRule might not be the correct name, because the function doesn't actually return a Rule but instead the application of the set of system rules (or the identity production if the Predecessor is a terminal). I think apply makes sense here.

The remaining set of changes I have are all function implementation related. You can leverage Prelude functions to achieve a lot in Haskell, doing so often makes your code more readable as common patterns get expressed by familiar names.

For instance, apply looks a lot like finding an element in an association list, which in the Prelude is provided by lookup :: (Eq a) => a -> [(a, b)] -> Maybe b. Digging a little further into base we can also deal with that Maybe using Data.Maybe.fromMaybe :: a -> Maybe a -> a.

apply :: Predecessor -> [Rule] -> Successor
apply p rules = fromMaybe (terminal p) $ lookup p rules

terminal :: Predecessor -> Successor
terminal p = [p]


The next thing I'd recognize is that lazy evaluation helps simplify dealing with potentially infinite sequences of values. Instead of generating a finite number of states in lsystemIterate, generate every state lazily and return the iteration requested.

states :: LSystem -> [String]
states (axiom, rules) = iterate (concatMap (`apply` rules)) axiom

after :: LSystem -> Int -> String
after l n = states l !! n


(You also seem to have some dead code, iterateIOMaybe.)

Code Snippets

readRule :: String -> Rule
readRule (x:' ':rest) = (x, rest)
readRule bad          = error $ "readRule: Can't parse " ++ show bad
type Predecessor = Char
type Successor   = String
type Rule        = (Predecessor, Successor)
getRule :: Predecessor -> [Rule] -> Successor
apply :: Predecessor -> [Rule] -> Successor
apply p rules = fromMaybe (terminal p) $ lookup p rules

terminal :: Predecessor -> Successor
terminal p = [p]
states :: LSystem -> [String]
states (axiom, rules) = iterate (concatMap (`apply` rules)) axiom

after :: LSystem -> Int -> String
after l n = states l !! n

Context

StackExchange Code Review Q#87567, answer score: 2

Revisions (0)

No revisions yet.