Elm has no genericity mechanisms for structures by now, like Haskell typeclasses, nor instance arguments.
Elm gives a parser error if you try to use type variables for structures like (m a) in a function.
Instead you have a few predefined, compiler bound, type variable categories.
(Examples tested with Elm version 0.18)
Syntax
Check it here.
Predefined stuff
Elm's predefined functions are at Basics, but there are additional default imports.
TypeVarCategories
Elm has predefined type variable categories that have implicit functionality. You specify a type var. category by means of a prefix. Check the compiler function categorizeVar
- type variables with "number" as prefix form a category that corresponds to Haskell's Num typeclass (a ring structure), defines (+), (-), (*), abs, negate, adding (^) and clamp to the pack. Check the link documentation for available instances.
- "comparable" type var category corresponds to Haskell's Ord functionality
- "appendable" type var category corresponds to Haskell's Semigroup with (++) as right associative infix operator for append
To extend the functionality with new functions based on the predefined ones for the category, you only have to categorize the type variable of the parameter using the category prefix.
Type differences
Elm's Int has the integer division as double slash (//) and mod as (%), and rem as rem. (/) is only for Floats
But there is a catch with (//). It does not return the integer part when dividing negatives. It behaves like haskell's quot truncating towards zero !! So it does not correspond to the euclidean division definition because it doesn't comply the rule stating that the remainder should be non‑negative.
import Html as H
import List as L
-- (//) behaves like `quot`, not like `div`
intDblSlashIsQuot : Int -> Int -> Bool
intDblSlashIsQuot denom num = abs (num // denom) == abs (-num // denom)
checkProperty = intDblSlashIsQuot 3
main = H.text <| toString <| L.map checkProperty [7, -2, 4, -8]
- Elm's Float (JavaScript's IEEE754 Number) corresponds to Haskell's Double
- Parameters with the type category number allow Int and Float as actual parameter types.
- Elm's data types use the clause keyword type. Type aliases are defined with "type alias".
- Elm has a special uninhabited type Never to be used as a Task error type parameter for tasks that cannot fail.
Some structure equivalences
- Elm's Maybe corresponds to Haskell's Maybe
- Elm's Result has an homomorphic corresponding in Haskell's Either
- Squared bracket literals [1,3,5] denote Elm's Lists, not Arrays.
-- zips are missing but easy done
zip : List a -> List b -> List (a,b)
zip = List.map2 (,)
zip3 = List.map3 (,,)
zip4 = List.map4 (,,,)
Elm's random access sequence Array is immutable, not like JS ones. A Haskell corresponding is Vector from Data.Vector.Unboxed
Elm's linear access sequence List
Elm's mapping Dict structure (dictionary) requires comparable key types (ordered domains). This includes Int, Float, Time, Char, String, and tuples or lists of comparable types. Its corresponding in Haskell is Data.Map.Strict where stored values are forcedly evaluated.
Elm's Set structure as a special case of Dict where the elements are the keys, has the same restriction.
Elm's Task structure is for effect actions that may fail. It implements Haskell's MonadError throwing errors with fail that skips the subsequent actions until the onError monadic catcher is evaluated, and its type (Task err a) would correspond to IO lifted to a MonadError compliant transformer, like (ExceptT err IO a).
Haskell range syntax is not supported in Elm. Specific functions are used: List.range. But you can define a dot-dot operator (..)
import Html as H
import List
(..) = List.range
infix 9 ..
main = H.text <| toString (1..7)
A Haskell style stepped range for Ints defining enumFromThenTo.
import Html as H
import List as L
import Debug as D
import Char as Ch
-- tailrec
intEnumFromThenToTR : List Int -> Int -> Int -> Int -> List Int
intEnumFromThenToTR acc ini nxt top =
if ini == nxt
then D.log "next must be different than initial" []
else if {- ini beyond top -} ini /= top &&
compare ini top /= compare ini nxt
then L.reverse acc
else intEnumFromThenToTR (ini :: acc) nxt (nxt + nxt - ini) top
intEnumFromThenTo = intEnumFromThenToTR [] -- using this seems to slow result yield
v0 = intEnumFromThenTo 9 7 1 -- [9,7,5,3,1]
v1 = intEnumFromThenToTR [] 9 7 1 -- not as elegant but maybe faster
-- refactored
intEnumFromThenTo2 : Int -> Int -> Int -> List Int
intEnumFromThenTo2 ini0 nxt0 top =
if ini0 == nxt0
then D.log "next must be different than initial" []
else let
step = nxt0 - ini0
go acc ini nxt =
if {- ini beyond top -} ini /= top &&
compare ini top /= compare ini nxt
then L.reverse acc
else go (ini :: acc) nxt (nxt + step)
in go [] ini0 nxt0
v2 = intEnumFromThenTo2 9 7 1
charEnumFromThenTo: Char -> Char -> Char -> List Char
charEnumFromThenTo ini0 nxt0 top =
if ini0 == nxt0
then D.log "next must be different than initial" []
else L.map Ch.fromCode <|
intEnumFromThenTo2 (Ch.toCode ini0) (Ch.toCode nxt0) (Ch.toCode top)
v3 = charEnumFromThenTo 'Y' 'W' 'A'
main = H.text <| toString (v2, v3)
Using partial application seems to yield result later in Elm's try-online vs. specifying all function arguments at once. Check it also with the program at the page bottom.
Functions
Single pattern definitions.
No where clauses.
Elm's Prelude is described here.
Elm's API search is here.
Haskell's id is called identity in Elm.
Haskell's const is called always in Elm.
Haskell's flip, curry and uncurry are named equally in Elm.
Haskell's function application Data.Function.($) corresponds to Elm's (<|)
Haskell's reverse application Data.Function.(&) corresponds to Elm's (|>)
Haskell's right to left composition Control.Category.(<<<) corresponds to Elm's (<<)
Haskell's left to right composition Control.Category.(>>>) corresponds to Elm's (>>)
Haskell's show corresponds to Elm's toString
Haskell's Data.Tuple fst, snd are defined in Elm's Basics, but they are named first, second in Elm's Tuple
Tuple constructors are defined for arity (<= 9), but the Tuple module advises against it use, proposing records instead.
import Html as H
v = (,,,,,,,,) 1 2 3 4 5 6 7 8 9 -- `_Tuple9` Ok, `_Tuple10` undefined
main = H.text (toString v)
Functions arity has a top of 9 arguments
usual ADT functions
Functor: Haskell Data.Functor.fmap corresponds to Elm structures' map function
Monad: Haskell's bind (>>=) corresponds to Elm structures' andThen flipped.
Applicative: With Elm structures' (map2, .., map5) you have the functionality that corresponds to Haskell's Control.Applicative (liftA2, ...) to combine the results of a string of actions as subsequent parameters.
For Task's map{N}, action side effects are serialized with andThen (Haskell's bind) so they rather correspond to Control.Monad.liftM{N} instead of Control.Applicative.liftA{N}.
Reductions, Traversals and Tail call optimization
>> The Elm compiler is able to do tail-call elimination on a function when any of the branches are a direct self-recursive call. See Tail-call elimination.
Lists
-- from Elm's core library List
foldl : (a -> b -> b) -> b -> List a -> b
foldl func acc list =
case list of
[] -> acc
x :: xs -> foldl func (func x acc) xs -- tail recursive
foldr : (a -> b -> b) -> b -> List a -> b
foldr = Native.List.foldr -- via JS list conversion toArray(xs)
-- map is based on foldr, so elements are traversed right to left
map : (a -> b) -> List a -> List b
map f xs = foldr (\x acc -> f x :: acc) [] xs
-- Task.sequence traverses left-to-right a list of Tasks giving a Task that returns the List of results. It uses map2 (like Applicative.liftA2 but serializing the side effects: Control.Monad.liftM2) to concatenate its results.
Desugaring Haskell Do blocks
Simulation of a Haskell do block, nesting subsequent monadic functions to have all lambda argument variables in scope. Try it in Elm's try online:
import Html as H exposing (Html)
import Maybe as M
-- Maybe.andThen : (a -> Maybe b) -> Maybe a -> Maybe b
-- simulation of a Haskell "do" block
monadic : Maybe Int
monadic = Just 1
|> M.andThen (\x -> Just 2
|> M.andThen (\y -> let y2 = y * y -- a `do block` let
in Just (x + y2)
|> M.andThen (\z -> Just (x + y2 + z) -- y2 still in scope
)))
-- rewriting it with (>>=)
(>>=) m f = M.andThen f m
infixl 1 >>=
monadic2 : Maybe Int
monadic2 = Just 1
>>= (\x -> Just 2
>>= (\y -> let y2 = y * y -- a `do block` let
in Just (x + y2)
>>= (\z -> Just (x + y2 + z) -- y2 still in scope
)))
main : Html String
main = H.text <| toString (monadic, monadic2)
Deferring computations - Lazy parameters
See also elm-lang/lazy. Lazyness memoisation is advised against. Have a look at the link's "Pitfalls" section regarding memory usage for memoization of lazy expressions and parameters.
import Html as H
-- lazyness without memoization
type Lazy a = Lazy (() -> a)
force : Lazy a -> a
force (Lazy f) = f ()
--------------
-- use:
maybeOrElse : Maybe a -> Lazy a -> a
maybeOrElse mbX lzY = case mbX of
Just x -> x
Nothing -> force lzY
v = maybeOrElse Nothing <| Lazy (\_ -> sqrt 2)
main = H.text <| toString v
A static "Hello world" page
import Html as H exposing (Html)
-- appendables (Semigroup) have (++) as infixr for append (See Basics)
main : Html msg
main = H.text <| "Hello " ++ "cruel world!"
Pages without effects
Web programs without effects follow the so called beginnerProgram structure as explained here.
Html.beginnerProgram : { model : model,
view : model -> Html msg,
update : msg -> model -> model }
-> Program Never model msg
main = Html.beginnerProgram { model = initialModel, view = view, update = update }
Pages with effects
Html.program : { init : (model, Cmd msg),
update : msg -> model -> (model, Cmd msg),
subscriptions : model -> Sub msg,
view : model -> Html msg }
-> Program Never model msg
Effect actions (tasks) are not evaluated directly but by sending a command to an Effect manager that will be handled differently based on their protocol processing state.
Effects, Effects managers and Message routing
Effect managers support protocol processing by handling its own message queue (selfMsg queue), and may send messages to the app main loop through the appMsg queue parameter of a Router object assigned to them.
You can interact with them sending commands and subscriptions specific to the effect manager which finally will send messages routed to your app main loop, and handled by the update function.
See the guide on Effects and the Platform Router
-- from the Platform module:
{-| Task: Represents asynchronous effects that may fail. It is useful for stuff like HTTP.
-}
type Task err ok = Task
{-| Router: An effect manager has access to a “router” that routes messages between the main app and your individual effect manager.
-}
type Router appMsg selfMsg = Router
{-| sendToApp: The effects manager module will be able to send the router a message for the main loop of your app. This message will be handled by the overall `update` function, just like events from `Html`.
-}
sendToApp : Router appMsg a -> appMsg -> Task err ()
{-| sendToSelf: The effects manager module will be able to send the router a message for its own message queue. This message will
be routed to the `onSelfMsg` function, where you can update the state of your effect manager as necessary.
-}
sendToSelf : Router a selfMsg -> selfMsg -> Task err ()
The (Cmd msg) and (Sub msg) types are the types of requests for commands or subscriptions sent to the effect managers (see below) that may send app messages to be processed by the update function.
Each effect manager exposes user entry points to build such requests.
-- from Platform.Cmd
{-| Cmd msg: the type of commands to Elm's effects
Every Cmd specifies (1) which effects you need access to and (2) the type of messages that will come back into your application.
-}
type Cmd msg
-- from Platform.Sub
{-| Sub msg: the type of commands to Elm's effects
Every Sub specifies (1) which effects you need access to and (2) the type of messages that will come back into your application.
-}
Elm has modules qualified by the effect keyword as effect managers. They handle Msgs from/to the run time system.
As example, the Websocket Effect Manager skeleton, expliciting which queue the message types are related:
effect module WebSocket where { command = MyCmd, subscription = MySub } exposing
( send
, listen
, keepAlive
)
...
-- MyCmd and MySub are parameterized by an appMsg type to encode app. message constructors.
type MyCmd appMsg = Send String String
type MySub appMsg = Listen String (String -> appMsg)
| KeepAlive String
-- | user entry calls to build command or subscription requests
send : String -> String -> Cmd appMsg
send url txt = command (Send url txt)
listen : String -> (String -> appMsg) -> Sub appMsg
listen url tagger = subscription (Listen url tagger)
-- ^ tagger: appMsg constructor to wrap the received String with
keepAlive : String -> Sub appMsg
keepAlive url = subscription (KeepAlive url)
-- | request processing automaton (input -> state -> state) callback
onEffects
: Platform.Router appMsg SelfMsg
-> List (MyCmd appMsg) -- cmd requests
-> List (MySub appMsg) -- subscription requests
-> State appMsg
-> Task Never (State appMsg)
onEffects router cmds subs state = ...
-- | State: The msg param in the State type is the appMsg of the type of the current subscriptions
type alias State msg =
{ sockets : SocketsDict
, queues : QueuesDict
, subs : SubsDict msg
}
-- | selfMsgs (type changed from Msg to SelfMsg to avoid confusion)
type SelfMsg
= GoodOpen String WS.WebSocket
| BadOpen String
| Receive String String
| Die String
-- | selfMsgs processing automaton callback
onSelfMsg
: Platform.Router appMsg SelfMsg
-> SelfMsg
-> State appMsg
-> Task Never (State appMsg)
onSelfMsg router selfMsg state = ...
-- | cmdMap called by Platform.Cmd.map
cmdMap : (a -> b) -> MyCmd a -> MyCmd b
-- | subMap called by Platform.Sub.map
subMap : (a -> b) -> MySub a -> MySub b
See WebSockets client program from the Elm's guide.
Other effect managers: Time (subscription only), Random (command only), other at core library or Effects pkg list
Running a Task
The type (Task err a) represents an action that may fail.
You may run a Task with the commands perform or attempt, that send a message to the appMsg queue upon finalisation, through the Task effect manager (See the module's source).
Within Effect managers, automaton callbacks are Tasks. You can also run tasks asynchronously through Process.spawn and abort them with Process.kill.
The structure Task implements Haskell's Monad and MonadError functionality as well, for computations that can throw errors whose type is determined by the type of the monad, here (Task err), where throwError bypasses subsequent actions until catchError is found.
- succeed -> Haskell Monad's return implementation
- andThen -> Haskell Monad's flipped bind impl.
- fail -> Haskell MonadError's throwError
- onError -> Haskell MonadError's catchError
- map -> Haskell Functor's fmap
- map2 -> Haskell Control.Monad's liftM2
- map3 -> Haskell Control.Monad's liftM3
- sequence -> Haskell Control.Monad sequence for a List container
effect module Task where { command = MyCmd } exposing
( Task -- the type
, perform, attempt -- cmds to run tasks
, succeed, andThen -- return, bind
, map, map2, map3, map4, map5 -- functor, liftM{N}
, sequence -- sequence a list of tasks
, fail, onError, mapError -- throwError, catchError
)
{-| Command to perform asynchronously a Task that cannot fail (err ~ Never) and send a msg to the appMsg queue
-}
perform : (a -> msg) -> Task Never a -> Cmd msg
perform toMessage task =
command (Perform (map toMessage task))
{-| Command to attempt asynchronously a task that might fail and send a msg chosen by the resultToMessage function
-}
attempt : (Result err a -> msg) -> Task err a -> Cmd msg
attempt resultToMessage task =
command (Perform (
task
|> andThen (succeed << resultToMessage << Ok) -- Ok is a constructor of type Result
|> onError (succeed << resultToMessage << Err) -- Err is a constructor of type Result
))
{-| Simple task generator that succeeds immediately when run.
A Haskell's Monad `return` implementation.
-}
succeed : a -> Task err a
{-| Tasks chaining.
A flipped version of Haskell's Monad `bind`
-}
andThen : (a -> Task x b) -> Task x a -> Task x b
{-| fail gives a task that fails immediately when run.
fail "file not found" : Task String a
fail: Monadic exception thrower, MonadError's throwError
-}
fail : err -> Task err a
{- onError: Monadic exceptions catcher, MonadError's catchError
-}
onError : (x -> Task y a) -> Task x a -> Task y a
{- mapError: to wrap the error type into a wider union one with a constructor
-}
mapError : (x -> y) -> Task x a -> Task y a
{- Functor map
-}
map : (a -> b) -> Task x a -> Task x b
map f taskA =
taskA |> andThen (\a -> succeed (f a))
{- Results combination of serialized tasks: Haskell's liftM2
-}
map2 : (a -> b -> c) -> Task x a -> Task x b -> Task x c
map2 f2 taskA taskB =
taskA |> andThen (\a -> taskB
|> andThen (\b -> succeed (f2 a b)
))
...
{-| form a Task from a list of tasks returning the list of results.
-}
sequence : List (Task x a) -> Task x (List a)
sequence tasks =
case tasks of
[] -> succeed []
task :: remainingTasks ->
map2 (::) task (sequence remainingTasks)
Adding tipical control functions for the type "Task"
module Task_Extra exposing (..)
import Task exposing (Task, succeed, andThen, map2)
-- lazyness without memoization
type Lazy a = Lazy (() -> a)
force : Lazy a -> a
force (Lazy f) = f ()
-- let's call traverse what in Haskell would be mapM or traverseM
traverse : (a -> Task err b) -> List a -> Task err (List b)
traverse f li = case li of
[] -> succeed []
x :: xs -> map2 (::) (f x) (traverse f xs)
-- side effects only
traverse_ : (a -> Task err ()) -> List a -> Task err ()
traverse_ f li = case li of
[] -> succeed ()
x :: xs -> f x |> andThen (\_ -> traverse_ f xs)
-- Haskell's monadic forM
for = flip traverse
for_ = flip traverse_
when : Bool -> Lazy (Task err ()) -> Task err ()
when cond lzTask =
if cond then force lzTask
else succeed ()
whenM : Lazy (Task err Bool) -> Lazy (Task err ()) -> Task err ()
whenM lzBoolTask lzTask =
force lzBoolTask |> andThen (\cond -> when cond lzTask)
-- replicate with Index
replicate : Int -> (Int -> Task err a) -> Task err (List a)
replicate n f =
if n <= 0 then succeed []
else let go i = -- i: 0..n
if (i < n) then map2 (::) (f i) <| go (i+1)
else succeed []
in go 0
-- side effects only
replicate_ : Int -> (Int -> Task err ()) -> Task err ()
replicate_ n f =
if n <= 0 then succeed ()
else let go i action = -- i: 0..n
if (i < n) then go (i+1) (f i) -- tail recursive
else succeed ()
in go 0 (succeed ())
Elm timing example: Comparing elapsed times of expressions with partial application vs complete number of arguments.
Check the function application templates and run this program in Elm's try-online.
import Html as H exposing (Html)
import Time as TM exposing (Time)
import Platform.Cmd
import Platform.Sub
import Task as TS exposing (Task)
import List as L
import Debug as D
-- (>>=) for Tasks
(>>=) m f = TS.andThen f m
infixl 1 >>=
type Lazy a = Lazy (() -> a)
force : Lazy a -> a
force (Lazy f) = f ()
type MyErr = MyErr String
type Msg = Start | Elapsed (Time, Time) | MsgErr String
type alias Model = {elapsed: Result String (Time, Time)}
intEnumFromThenToTR : List Int -> Int -> Int -> Int -> List Int
intEnumFromThenToTR acc ini nxt top =
if ini == nxt then D.log "next must be different than initial" []
else if {- ini beyond top -} ini /= top &&
compare ini top /= compare ini nxt
then L.reverse acc
else intEnumFromThenToTR (ini :: acc) nxt (nxt + nxt - ini) top
intEnumFromThenTo = intEnumFromThenToTR [] -- using partial application seems to slow execution
v0 = intEnumFromThenTo 1003 1001 1
v1 = intEnumFromThenToTR [] 1003 1001 1 -- maybe faster
-- evaluate the deferred lzA parameter N times
iterateM_ : Int -> Lazy a -> Task MyErr ()
iterateM_ n lzA =
case compare n 0 of
LT -> TS.fail <| MyErr "iterateM_: n must be positive"
EQ -> TS.succeed ()
GT -> let -- tail recursive, evaluating the action as parameter
go m action =
if m == 0 then TS.succeed ()
else go (m-1) (TS.succeed (force lzA)
>>= (\_ -> TS.succeed ()))
in go n (TS.succeed ())
-- time N evaluations of lzA
timeItN : Int -> Lazy a -> Task MyErr Time
timeItN n lzA = TM.now
>>= (\ tmIni -> iterateM_ n lzA
>>= (\ _ -> TM.now
>>= (\ tmFinal -> TS.succeed (tmFinal - tmIni)
)))
timeV : List Int -> Task MyErr Time
timeV v = timeItN 1000 <| Lazy (\_ -> L.sum v)
targetsTimes : Task MyErr (Time, Time)
targetsTimes = timeV v0
>>= (\ t1 -> timeV v1
>>= (\ t2 -> TS.succeed (t1, t2)
))
timesResultToMsg : Result MyErr (Time, Time) -> Msg
timesResultToMsg res =
case res of
Ok v -> Elapsed v
Err (MyErr str) -> MsgErr str
view : Model -> Html msg
view model =
case model.elapsed of
Err str -> H.div [] [H.text <| "Error: " ++ str]
Ok (t1, t2) -> -- arrange (t1, t2) in a List to map units once
let res = L.map TM.inMilliseconds [t1, t2]
in H.div [] [H.text <| toString res]
update : Msg -> Model -> (Model, Cmd Msg)
update msg mdl = case msg of
Start -> (mdl, TS.attempt timesResultToMsg targetsTimes)
Elapsed tm -> ({ mdl | elapsed = Ok tm}, Cmd.none)
MsgErr str -> ({ mdl | elapsed = Err str}, Cmd.none)
init : (Model, Cmd Msg)
init = ({elapsed = Ok (0, 0)},
TS.perform (\_ -> Start) (TS.succeed ())) -- send Start msg
main = H.program { init = init, update = update,
view = view, subscriptions = (\_ -> Sub.none)}
More info
- Cabal-like project file, JSON styled (elm-package.json)
- A library project file
- Installing packages
- The Elm Book - An Introduction to Elm
- Forms in Elm
Other client side Model-View-Controller frameworks
PureScript offers alternatives for designing Web User Interfaces much nearer to the language power of Haskell and much lighter weight than GHCJS (no GHC RTS emulation).