@listrophy
Please visit ellie-app.com
[3, 1, 2].sort
# => [1, 2, 3]
List.sort [3, 1, 2]
-- => [1, 2, 3]
["a", "b", "c"].join " "
# => "a b c"
String.join " " ["a", "b", "c"]
-- => "a b c"
Note: No commas between args
Note 2: "Data" acted upon is last arg
x = [3, 1, 2]
y = x.sort # Elm equivalent exists
x.sort! # Elm equivalent does not exist
List.sort [3, 1, 2]
Turns out, they're not actually that bad!
aList = [
foo,
bar,
baz
]
aList =
[ foo
, bar
, baz
]
aNumber = 42
errorMessage =
if aNumber
"An error of type #{aNumber} occurred."
end
aNumber = 42
errorMessage =
if aNumber /= 0 then
"An error of type " ++ (toString aNumber) ++ " occurred."
else
""
Note: There is no "truthy" or "falsey", only "True" or "False"
Note: No automatic string conversion
Note: All code-paths must yield same type
Note: Indentation matters!
aValue : String
aValue =
"foo"
aFunc : String -> String
aFunc stringArg =
String.toUpper (String.trim stringArg)
require 'foo'
# Now everything from 'foo' is available,
# INCLUDING everything that 'foo' itself required
import Foo
-- Now everything from Foo is available but still namespaced
-- under "Foo."
--
-- Nothing that Foo imported in its own file is available here
-- unless it's been explicitly "exposed"
# in file "foo.rb"
class Foo; end
# files that require 'foo' have access to Foo
-- in file "Foo.elm"
module Foo exposing (foo, foo2)
-- files that "import Foo" can only access
-- functions "foo" and "foo2"
-- in file "Foo.elm"
module Foo exposing (..)
-- files that import 'Foo' can access all functions in Foo
require 'foo'
# Now everything from 'foo' is available,
# INCLUDING everything that 'foo' itself required
import Foo
import Bar as B
import Baz exposing (..)
import Qux exposing (corge)
Foo.func "a" -- call "func" from module Foo with arg "a"
B.func "a" -- call "func" from module Bar with arg "a"
bazzle "a" -- call "bazzle" with arg "a",
-- which apparently came from Baz
corge "a" -- call "corge" from module Qux, with arg "a"
if foo?
doSomething
else
doOtherThing
end
case foo of
True ->
doSomething
False ->
doOtherThing
Note: The compiler ensures case statements are exhaustive!
<% if current_user %>
<%= button_to "Log out", destroy_session_path %>
<% else %>
<%= render partial: 'login' %>
<% end %>
case model.currentUser of
User _ ->
Html.button
[ onClick SignOut ]
[ text "Log out" ]
Anonymous ->
loginView
Admin _ ->
adminSimulateUserOrLogoutView
var logDiv = function(index, div) {
console.log(div.innerHTML);
};
$('div').each(logDiv);
List.map String.toUpper ["foo", "bar", "baz"]
Just a grab-bag to show syntax:
module Trilean
exposing
( Reason
, Trilean
, isSure
, reason
)
-- ...
-- ...
type alias Reason =
String
type Trilean
= Yes
| No
| Other Reason
-- ...
-- ...
isSure : Trilean -> Bool
isSure trilean =
case trilean of
Yes ->
True
_ ->
False
-- ...
-- ...
reason : Trilean -> Maybe String
reason trilean =
case trilean of
Other str ->
Just str
_ ->
Nothing
-- ...
String
Maybe String
Important: You cannot use a Maybe String like a String any more than you can use an Array of Strings like a single String in Ruby.
singleDigitInts : List Int
singleDigitInts =
[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
-- or `List.range 0 9`
doubleList : List Int -> List Int
doubleList list =
List.map (\int -> 2 * int) list
-- in Ruby:
-- list.map { |int| 2 * int }
map : (a -> b) -> ConcreteType a -> ConcreteType b
-- for List
map : (a -> b) -> List a -> List b
-- for Maybe
map : (a -> b) -> Maybe a -> Maybe b
-- Elm does not have Higher-Kinded Types, so this doesn't work:
map<M> : (a -> b) -> M a -> M b
Note: Lack of Higher-Kinded Types in this situation means you can't have a generic map function that works on anything "mappable."
normalizeString : String -> String
normalizeString input =
let
trimmed = String.trim input
lower = String.toLower trimmed
in
String.padLeft 8 ' ' lower
ok, ok, "forward function application"
normalizeString : String -> String
normalizeString input =
let
trimmed = String.trim input
lower = String.toLower trimmed
in
String.padLeft 8 ' ' lower
alsoNormalizeString : String -> String
alsoNormalizeString input =
input
|> String.trim
|> String.toLower
|> String.padLeft 8 ' '
You can supply only some arguments to any function, though only from left-to-right.
-- type signature of String.padLeft:
padLeft : Int -> Char -> String -> String
-- effective type signature of String.padLeft:
padLeft : Int -> (Char -> (String -> String))
Therefore, if you know the padding width, but not the padding char or the string, you can do:
String.padLeft 8
-- <function> : Char -> String -> String
alsoNormalizeString : String -> String
alsoNormalizeString input =
input
|> String.trim
|> String.toLower
|> String.padLeft 8 ' '
moreNormalizeString : String -> String
moreNormalizeString =
String.trim >> String.toLower >> String.padLeft 8 ' '
Note: This is kinda advanced and not completely necessary for the purposes of this workshop
parenNormalizeString : String -> String
parenNormalizeString input =
String.padLeft 8 ' ' (String.toLower (String.trim input))
anotherNormalizeString : String -> String
anotherNormalizeString input =
String.padLeft 8 ' ' <| String.toLower <| String.trim input
Note: Much like in Ruby, the balance between using and not using parens is subject to much debate
x = 42 -- Int
y = 3.14 -- Float
c = '.' -- Char (rarely used)
s = "hello world" -- String
l = [] -- List a
t = (1, "foo", 4.0) -- Tuple of type (Int, String, Float)
-- Record of type { foo : String, bar : Int }
r = { foo = "foo", bar = 123}
-- Bool
b1 = True
b2 = False
-- Maybe a
-- here, "Maybe String"
m1 = Just "a string"
m2 = Nothing
-- Result a b
-- here, "Result String Int"
r1 = Ok 42
r2 = Err "error message"
-- ours
type Trilean
= Yes
| No
| Other Reason
-- builtin
type Result a b
= Err a
| Ok b
type Maybe a
= Just a
| Nothing
type Maybe a
= Just a
| Nothing
-- Type constructors
Just : a -> Maybe a
Nothing : Maybe a
type Result a b
= Err a
| Ok b
-- List
first : List a -> Maybe a
-- String
join : String -> List String -> String
-- Json.Decode
decodeString : Decoder a -> String -> Result String a
-- List
filter : (a -> Bool) -> List a -> List a
main =
Html.beginnerProgram
{ model = model
, view = view
, update = update
}
model : Model
model = ...
view : Model -> Html Msg
view model = ...
update : Msg -> Model -> Model
update msg model = ...
main =
Html.program
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
init : (Model, Cmd Msg)
view : Model -> Html Msg
update : Msg -> Model -> (Model, Cmd Msg)
subscriptions : Model -> Sub Msg
type alias Model =
{ username : String
}
init : (Model, Cmd Msg)
init =
( { username = "" }
, fetchUserList
)
fetchUserList : Cmd Msg
fetchUserList =
Http.send ...
type alias Model =
{ username : String
}
type Msg
= UsernameChanged String
| ...
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
UsernameChanged newUsername ->
({model | username = newUsername}, Cmd.none)
... ->
...
subscriptions : Model -> Sub Msg
subscriptions model =
if model.showClock then
Time.every (1 * second) TimerTicked
else
Sub.none
view : Model -> Html Msg
view model =
Html.button
[ onClick CalculateClicked ]
[ text "do a calculation" ]
-- some "typical" Msg values
type Msg
= UsernameChanged String -- Html Msg
| FieldChanged String String -- Html Msg
| CalculateClicked -- Html Msg
| TimerTicked Time -- Sub Msg
| RandomNumberChosen Int -- Cmd Msg
| UserListRecieved (WebData (List User)) -- Cmd Msg
package.elm-lang.org
In order to track my team's score
As a score keeper
I want to increment their score
module Main exposing (main)
import Html exposing (..)
main =
beginnerProgram
{ model = initialModel
, view = view
, update = update
}
type alias Model = Int
initialModel : Model
initialModel =
0
-- insert at top:
import Html.Events exposing (onClick)
--
type Msg
= Increment
view : Model -> Html Msg
view model =
div
[]
[ button [ onClick Increment ] [ text "+" ]
, text (toString model)
]
update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
model + 1
In order to track my basketball team's score
As my team's scorekeeper
I want to increment the score by 1, 2 or 3
-- No change!
type alias Model = Int
initialModel : Model
initialModel =
0
type Msg
= FreeThrow
| JumpShot
| ThreePointer
view : Model -> Html Msg
view model =
div
[]
[ button [ onClick FreeThrow ] [ text "free throw" ]
, button [ onClick JumpShot ] [ text "jump shot" ]
, button [ onClick ThreePointer ] [ text "trey" ]
, text (toString model)
]
update : Msg -> Model -> Model
update msg model =
case msg of
FreeThrow ->
model + 1
JumpShot ->
model + 2
ThreePointer ->
model + 3
FreeThrow, JumpShot, and ThreePointer seem a bit redundant. Let's use one Msg to represent all three.
-- No change!
type alias Model = Int
initialModel : Model
initialModel =
0
type Msg
= AddPoints Int
view : Model -> Html Msg
view model =
div
[]
[ button [ onClick (AddPoints 1) ] [ text "free throw" ]
, button [ onClick (AddPoints 2) ] [ text "jump shot" ]
, button [ onClick (AddPoints 3) ] [ text "trey" ]
, text (toString model)
]
update : Msg -> Model -> Model
update msg model =
case msg of
AddPoints int ->
model + int
I accidentally pressed the wrong button! I need to undo a score.
type alias Model = List Int
initialModel : Model
initialModel =
[]
type Msg
= AddPoints Int
| Undo
view : Model -> Html Msg
view model =
div
[]
[ button [ onClick (AddPoints 1) ] [ text "free throw" ]
, button [ onClick (AddPoints 2) ] [ text "jump shot" ]
, button [ onClick (AddPoints 3) ] [ text "trey" ]
, text (toString (List.sum model))
, button [ onClick Undo ] [ text "undo!" ]
]
update : Msg -> Model -> Model
update msg model =
case msg of
AddPoints int ->
int :: model
Undo ->
List.drop 1 model
Let's move away from "beginnerProgram"
module Main exposing (main)
import Html exposing (..)
import Date
import Time
main =
program
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
type alias Model = Maybe Time.Time
init : (Model, Cmd Msg)
init =
( Nothing
, Cmd.none
)
type Msg
= Tick Time.Time
subscriptions : Model -> Sub Msg
subscriptions model =
Time.every (100 * Time.millisecond) Tick
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
Tick t ->
(Just (Date.fromTime t), Cmd.none)
view : Model -> Html Msg
view model =
div [] <|
case model of
Just d ->
[ text <| toString (Date.hour d)
, text ":"
, text <| toString (Date.minute d)
, text ":"
, text <| toString (Date.second d)
]
Nothing ->
[ text "don't know yet!" ]
Let's talk to an API!
https://github.com/Giphy/GiphyAPI
module Main exposing (..)
import Html exposing (..)
import Html.Attributes exposing (src)
import Html.Events exposing (onClick)
import Http
import Json.Decode as JD
main =
program
{ init = init
, view = view
, update = update
, subscriptions = (\model -> Sub.none)
}
type alias Model =
{ url : Maybe String
, error : Maybe String
}
init : ( Model, Cmd Msg )
init =
( Model Nothing Nothing, Cmd.none )
type Msg
= Fetch
| Display (Result Http.Error String)
view : Model -> Html Msg
view model =
div []
[ button [ onClick Fetch ] [ text "Fetch!" ]
, text <| Maybe.withDefault "" model.error
, imageView model.url
]
imageView : Maybe String -> Html Msg
imageView mString =
case mString of
Just url ->
img [ src url ] []
Nothing ->
text "not fetched"
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Fetch ->
( model, doFetch )
Display (Ok url) ->
( { model | url = Just url, error = Nothing }
, Cmd.none
)
Display (Err err) ->
( { model | error = Just <| toString err }
, Cmd.none
)
randomUrl : String
randomUrl =
"https://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&rating=g"
doFetch : Cmd Msg
doFetch =
let
decoder =
JD.at [ "data", "fixed_height_small_url" ] JD.string
in
Http.get randomUrl decoder
|> Http.send Display
A multiplayer, cooperative game to
survive incoming fields of asteroids
type alias Model =
{ myPosition : Float
, laser : Maybe Position
, asteroids : List Position
, score : Maybe Int
, error : Maybe String
, cable : ActionCable Msg
, keysDown : Set.Set Keyboard.KeyCode
}
type alias Position =
{ x : Float
, y : Float
}
type Msg
= CableMsg ACMsg.Msg
| Tick Time.Time
| CableConnected ()
| KeyDown Keyboard.KeyCode
| KeyUp Keyboard.KeyCode
| DataReceived ID.Identifier JD.Value
| GenerateAsteroid Position
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
CableMsg msg_ -> ...
CableConnected _ -> ...
Tick time -> ...
KeyDown 32 -> ...
KeyDown keyCode -> ...
KeyUp keyCode -> ...
DataReceived _ jsonValue -> ...
GenerateAsteroid position -> ...
That's it!
(we do score handling on the server asynchronously)
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ ActionCable.listen CableMsg model.cable
, AF.diffs Tick
, Keyboard.downs KeyDown
, Keyboard.ups KeyUp
]
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
CableMsg msg_ -> ...
CableConnected _ -> ...
Tick time -> ...
KeyDown 32 -> ...
KeyDown keyCode -> ...
KeyUp keyCode -> ...
DataReceived _ jsonValue -> ...
GenerateAsteroid position -> ...
CableMsg msg_ ->
ActionCable.update msg_ model.cable
|> Tuple.mapFirst (\c -> { model | cable = c })
-- or
CableMsg msg_ ->
let
(newCable, cmd) =
ActionCable.update msg_ model.cable
in
( { model | cable = newCable }, cmd)
CableConnected _ ->
joinGame model
joinGame : Model -> ( Model, Cmd Msg )
joinGame model =
case subscribeToGame model of
Ok model_cmd ->
model_cmd
Err err ->
( { model
| error = Just <| ActionCable.errorToString err
}
, Cmd.none
)
subscribeToGame :
Model
-> Result ActionCable.ActionCableError ( Model, Cmd Msg )
subscribeToGame model =
let
setCable ( newCable, cmd ) =
( { model | cable = newCable }, cmd )
in
ActionCable.subscribeTo identifier model.cable
|> Result.map setCable
identifier : ID.Identifier
identifier =
ID.newIdentifier "GamesChannel" []
KeyDown 32 ->
( fire model, Cmd.none )
KeyDown keyCode ->
( { model | keysDown = Set.insert keyCode model.keysDown }
, Cmd.none
)
KeyUp keyCode ->
( { model | keysDown = Set.remove keyCode model.keysDown }
, Cmd.none
)
fire : Model -> Model
fire ({ laser, myPosition } as model) =
let
newLaser =
Just <| Position leftEdge (2.5 * myPosition)
in
case laser of
Nothing ->
{ model | laser = newLaser }
Just realLaser ->
if realLaser.x < 500 then
model
else
{ model | laser = newLaser }
Tick time ->
tick time model
tick : Time.Time -> Model -> ( Model, Cmd Msg )
tick time model =
model
|> moveShipAndLaser
|> moveAsteroids
|> blowUpAsteroid
|> postScore
|> (\( m, a, cmd ) -> ( m, regenerateAsteroids a cmd ))
moveShipAndLaser : Model -> Model
moveShipAndLaser ({ myPosition } as model) =
let
moveMe =
if Set.member 38 model.keysDown then
2.0
else if Set.member 40 model.keysDown then
-2.0
else
0.0
in
{ model
| myPosition = myPosition + moveMe
, laser = Maybe.map moveLaser model.laser
}
moveLaser : Position -> Position
moveLaser { x, y } =
{ x = x + 10, y = y }
moveAsteroids : Model -> ( Model, List Position )
moveAsteroids model =
let
move { x, y } =
{ x = x - 1, y = y }
( kept, past ) =
model.asteroids
|> List.map move
|> List.partition (\pos -> pos.x > leftEdge - 50)
in
( { model | asteroids = kept }, past )
blowUpAsteroid :
( Model, List Position )
-> ( Model, List Position, List Position )
blowUpAsteroid ( model, past ) =
case model.laser of
Nothing ->
( model, past, [] )
Just laser ->
...
Just laser ->
let
( blownUp, keep ) =
List.partition
(detectCollision laser)
model.asteroids
in
if List.length blownUp > 0 then
( { model | asteroids = keep, laser = Nothing }
, past
, blownUp
)
else
( model, past, [] )
detectCollision : Position -> Position -> Bool
detectCollision laser asteroid =
asteroidRadius >
(sqrt <|
(asteroid.x - laser.x) ^ 2
+ (asteroid.y - laser.y) ^ 2
)
postScore :
( Model, List Position, List Position )
-> ( Model, List Position, Cmd Msg )
postScore ( model, past, blownUp ) =
( model
, List.append past blownUp
, sendScoreUpdate (List.length blownUp) model.cable
)
sendScoreUpdate : Int -> ActionCable Msg -> Cmd Msg
sendScoreUpdate int cable =
if int > 0 then
Result.withDefault Cmd.none <|
ActionCable.perform
"scoreUpdate"
[ ( "score", JE.int int ) ]
identifier
cable
else
Cmd.none
regenerateAsteroids : List Position -> Cmd Msg -> Cmd Msg
regenerateAsteroids asteroids existingCmd =
let
regen _ =
Random.generate
GenerateAsteroid
asteroidPositionGenerator
in
asteroids
|> List.map regen
|> (::) existingCmd
|> Cmd.batch
asteroidPositionGenerator : Random.Generator Position
asteroidPositionGenerator =
let
xGenerator =
Random.float 500.0 1500.0
yGenerator =
Random.float -220.0 220.0
in
Random.map2 Position xGenerator yGenerator
Are we there yet?
Two left:
GenerateAsteroid position ->
( { model | asteroids = position :: model.asteroids }
, Cmd.none
)
DataReceived _ jsonValue ->
( dataReceived jsonValue model, Cmd.none )
dataReceived : JD.Value -> Model -> Model
dataReceived json model =
json
|> JD.decodeValue (JD.field "score" JD.int)
|> Result.toMaybe
|> (\newScore -> { model | score = newScore })
Now, on to the view!
view : Model -> Html Msg
view model =
Element.toHtml <|
C.collage 1000 500 <|
gameItems model
gameItems : Model -> List C.Form
gameItems model =
List.concat
[ [ space
, connectedSignal model
, scoreView model.score
, shipView model.myPosition
]
, List.map asteroidView model.asteroids
, Maybe.withDefault [] <|
Maybe.map (laserView >> List.singleton) model.laser
]
space : C.Form
space =
C.rect 1000 500
|> C.filled Color.black
connectedSignal : Model -> C.Form
connectedSignal { cable } =
let
colorFill =
if Dict.isEmpty <| ActionCable.subscriptions cable then
Color.gray
else
Color.green
in
C.circle 5.0
|> C.filled colorFill
|> C.move ( 490, -240 )
scoreView : Maybe Int -> C.Form
scoreView int =
int
|> Maybe.map toString
|> Maybe.withDefault "?"
|> (++) "Score: "
|> Text.fromString
|> Text.color Color.white
|> C.text
|> C.move (( 420.0, 240 ))
shipView : Float -> C.Form
shipView myPosition =
C.polygon [ ( -10, -5 ), ( -10, 5 ), ( 10, 0 ) ]
|> C.filled Color.white
|> C.move ( leftEdge, (250.0 / 100.0 * myPosition) )
laserView : Position -> C.Form
laserView laser =
C.rect 20.0 2.0
|> C.filled Color.white
|> C.move ( laser.x, laser.y )
asteroidView : Position -> C.Form
asteroidView position =
C.ngon 7 asteroidRadius
|> C.filled Color.gray
|> C.move ( position.x, position.y )
init : ( Model, Cmd Msg )
init =
( { myPosition = 0.0
, laser = Nothing
, asteroids = []
, score = Nothing
, error = Nothing
, cable = initCable
, keysDown = Set.empty
}
, requestRandomAsteroidPositions
)
initCable : ActionCable Msg
initCable =
ActionCable.initCable "ws://localhost:3000/cable"
|> ActionCable.withDebug True
|> ActionCable.onWelcome (Just CableConnected)
|> ActionCable.onDidReceiveData (Just DataReceived)
requestRandomAsteroidPositions : Cmd Msg
requestRandomAsteroidPositions =
Random.generate GenerateAsteroid asteroidPositionGenerator
|> List.repeat 12
|> Cmd.batch
Ignoring those little helper functions
Brad Grzesiak: @listrophy
Bendyworks: @bendyworks
Elm: elm-lang.org