However, as I began to think about it more, I began to wonder if I was missing the point of the wonderful elegance of the Haskell type system. Putting it another way, I think there's a more elegant solution than the one proposed in my last post:
Haskell Code:
-- ClarityTake2.hs
type City = String
sanFrancisco :: City
sanFrancisco = "San Francisco, CA"
denver :: City
denver = "Denver, CO"
type CatName = String
moonlight :: CatName
moonlight = "Moonlight"
data From a = From a deriving (Show)
data To a = To a deriving (Show)
data Who a = Who a deriving (Show)
fly :: Who CatName -> From City -> To City -> String
fly (Who n) (From c1) (To c2) = n ++ " flies from " ++ c1 ++ " to " ++ c2
data Travel t1_who t2_place = Travel {
who :: t1_who,
from :: t2_place,
to :: t2_place
}
fly2 :: Travel CatName City -> String
fly2 t = (who t) ++ " flies from " ++ (from t) ++ " to " ++ (to t)
main :: IO ()
main = do
putStrLn $ fly (Who moonlight) (From denver) (To sanFrancisco)
putStrLn $ fly2 (Travel {who=moonlight, from=denver, to=sanFrancisco})
-- main =>
-- Moonlight flies from Denver, CO to San Francisco, CA
-- Moonlight flies from Denver, CO to San Francisco, CA
So in the code above, we use the type system itself to make it clear what we're trying to do. Why does this or should this work? Well, if one is using the excellent ghci (Glasgow Haskell Compiler interpreter), then one only needs to type ":i
GHCi, version 6.10.1: http://www.haskell.org/ghc/ :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer ... linking ... done.
Loading package base ... linking ... done.
Prelude> :l ./ClarityTake2.hs
[1 of 1] Compiling Main ( ClarityTake2.hs, interpreted )
Ok, modules loaded: Main.
*Main> :i fly
fly :: Who CatName -> From City -> To City -> String
-- Defined at ClarityTake2.hs:21:0-2
*Main> :i fly2
fly2 :: Travel CatName City -> String
-- Defined at ClarityTake2.hs:30:0-3
*Main> :i Travel
data Travel t1_who t2_place
= Travel {who :: t1_who, from :: t2_place, to :: t2_place}
-- Defined at ClarityTake2.hs:23:5-10
*Main> putStrLn $ fly2 (Travel {to="New York, NY", from=denver, who="Mittens"})
Mittens flies from Denver, CO to New York, NY
In the interactive session above, we can use the "info" command (:i
Unfortunately, however, although the type names have been clearer, we still have to remember argument order if we just use plain types. This is where the record syntax comes to the rescue. The "Travel t1 t2" type holds all of the function arguments which can be obtained using the accessor functions "who", "from", and "to".
By doing a little mental gymnastics piecing together the ":i Travel" and ":i fly2" information, we can see that Travel defines "who" as a "CatName" (standing in for t1_who) and "from"/"to" as type "City" (standing in for "t2_place"). The record arrays give then give us the flexibility to define arguments in whatever order we desire. As a last example, let's look at the last call to fly2 in the ghci interactive session above. Here, Mittens (another super cat) will fly from Denver to New York. However, we were able to call the function with arguments in a different order! This could be handy.
The nice thing about both solutions (Record syntax and descriptive type constructors like "From a" and "To a") is that the code readability is greatly enhanced. One can now come upon a piece of code like the following, and quickly parse out what it's doing just by reading it and that's nice:
drive (Who michael) (From denver) (To ftCollins) -- descriptive type constructors
drive (Travel {who=michael, from=denver, to=ftCollins}) -- record syntax

0 comments:
Post a Comment