├── .gitignore
├── src
├── Exercise016HTML.elm
├── Exercise015ModuleName.elm
├── Exercise010String.elm
├── Exercise011StringConcat.elm
├── Exercise012IntToString.elm
├── Exercise013Functions.elm
├── Exercise017StyledHTML.elm
├── Exercise014Signatures.elm
├── Exercise018CustomTypes.elm
├── Exercise019CustomTypesWithArguments.elm
├── Exercise020TEA.elm
├── Exercise021CounterReset.elm
├── Exercise022ListRepeat.elm
├── Exercise023ListMap.elm
├── Exercise027Time.elm
├── Exercise024ListPatternMatching.elm
├── Advanced
│ ├── README.md
│ ├── ExerciseDraw.elm
│ └── ExerciseMemoryGame.elm
├── Exercise025Http.elm
└── Exercise026HttpDecoder.elm
├── resources
├── quote-1.txt
├── quote-2.txt
├── quote-3.txt
└── elm-logo.svg
├── elm.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | elm-stuff/
3 |
--------------------------------------------------------------------------------
/src/Exercise016HTML.elm:
--------------------------------------------------------------------------------
1 | module Exercise016HTML exposing (..)
2 |
3 |
4 | main =
5 | "Hello World!"
6 |
--------------------------------------------------------------------------------
/src/Exercise015ModuleName.elm:
--------------------------------------------------------------------------------
1 | module Exercise015 exposing (..)
2 |
3 | import Html
4 |
5 |
6 | main =
7 | Html.text "Hello World!"
8 |
--------------------------------------------------------------------------------
/src/Exercise010String.elm:
--------------------------------------------------------------------------------
1 | module Exercise010String exposing (main)
2 |
3 | import Html
4 |
5 | main = Html.text "A very simple first exercise...
--------------------------------------------------------------------------------
/resources/quote-1.txt:
--------------------------------------------------------------------------------
1 | I've been shipping Elm applications to production at Mozilla and am doing so right now for the French government. I see no risks, only benefits.
2 | – Nicolas Perriault (@n1k0)
--------------------------------------------------------------------------------
/src/Exercise011StringConcat.elm:
--------------------------------------------------------------------------------
1 | module Exercise011StringConcat exposing (..)
2 |
3 | import Html
4 |
5 |
6 | hello =
7 | "Hello " + "world"
8 |
9 |
10 | main =
11 | Html.text hello
12 |
--------------------------------------------------------------------------------
/src/Exercise012IntToString.elm:
--------------------------------------------------------------------------------
1 | module Exercise012IntToString exposing (main)
2 |
3 | import Html exposing (Html)
4 |
5 |
6 | main =
7 | Html.text ("The multiplication of 19876 by 34678 results in " ++ result)
8 |
9 |
10 | result =
11 | 19876 * 34678
12 |
--------------------------------------------------------------------------------
/resources/quote-2.txt:
--------------------------------------------------------------------------------
1 | Enfin on peut faire du dev front facilement, de manière ultra-fiable, avec des assets minuscules, de très bonnes perfs, et sans se sentir périmé au bout de 6 mois parce qu'il y a un nouveau framework à la mode. Elm permet de construire sur le long terme.
2 | – @ccomb
3 |
--------------------------------------------------------------------------------
/src/Exercise013Functions.elm:
--------------------------------------------------------------------------------
1 | module Exercise013Functions exposing (..)
2 |
3 | import Html exposing (div, text)
4 |
5 |
6 | add a b =
7 | a + b
8 |
9 |
10 | main =
11 | div []
12 | [ div [] [ text ("5 plus 6 equals " ++ String.fromInt (add 5 6)) ]
13 | , div [] [ text ("5 times 6 equals " ++ String.fromInt (multiply 5 6)) ]
14 | ]
15 |
--------------------------------------------------------------------------------
/src/Exercise017StyledHTML.elm:
--------------------------------------------------------------------------------
1 | module Exercise017StyledHTML exposing (..)
2 |
3 | import Html exposing (li, text, ul)
4 | import Html.Attributes exposing (id, style)
5 |
6 |
7 | main =
8 | ul []
9 | [ li [ style "color" "red" ] [ text "This text is red" ]
10 | , li [ id "greenText", style "color-green" ] [ text "This text is green (hopefully)" ]
11 | ]
12 |
--------------------------------------------------------------------------------
/resources/quote-3.txt:
--------------------------------------------------------------------------------
1 | Quand on code une appli pour un client et qu'on doit assurer la maintenance à moyen/long terme sans faire d'évolutions, la stabilité de l'écosystème Elm rend la chose extrêmement confortable. On n'a plus peur de modifier une appli que personne n'a touché depuis des années, ce qui n'était pas le cas avec les autres frameworks. Pour moi avec Elm c'est la fin de l'obsolescence programmée du code JavaScript !
2 | – @cmdevienne
--------------------------------------------------------------------------------
/src/Exercise014Signatures.elm:
--------------------------------------------------------------------------------
1 | module Exercise014Signatures exposing (..)
2 |
3 | import Html exposing (div, text)
4 |
5 |
6 | add : Int -> Int -> Int
7 | add a b =
8 | a + b
9 |
10 |
11 | multiply : Int
12 | multiply a b =
13 | a * b
14 |
15 |
16 | main =
17 | div []
18 | [ div [] [ text ("5 plus 6 equals " ++ String.fromInt (add 5 6)) ]
19 | , div [] [ text ("5 times 6 equals " ++ String.fromInt (multiply 5 6)) ]
20 | ]
21 |
--------------------------------------------------------------------------------
/src/Exercise018CustomTypes.elm:
--------------------------------------------------------------------------------
1 | module Exercise018CustomTypes exposing (..)
2 |
3 | import Html exposing (li, text, ul)
4 | import Html.Attributes exposing (id, style)
5 |
6 |
7 | type Color
8 | = Red
9 | | Green
10 |
11 |
12 | main =
13 | ul []
14 | [ li [ style "color" (colorToString Red) ] [ text "This text is red" ]
15 | , li [ style "color" (colorToString Green) ] [ text "This text is green (hopefully)" ]
16 | ]
17 |
18 |
19 | colorToString : Color -> String
20 | colorToString color =
21 | case color of
22 | Red ->
23 | "red"
24 |
--------------------------------------------------------------------------------
/src/Exercise019CustomTypesWithArguments.elm:
--------------------------------------------------------------------------------
1 | module Exercise019CustomTypesWithArguments exposing (..)
2 |
3 | import Html exposing (li, text, ul)
4 | import Html.Attributes exposing (id, style)
5 |
6 |
7 | type Shape
8 | = Point
9 | | Square Float
10 | | Rectangle Float Float
11 |
12 |
13 | main =
14 | Html.text
15 | ("A rectangle with sides 5 cm and 3 cm has an area of "
16 | ++ String.fromFloat (calculateArea (Rectangle 5 3))
17 | ++ " cm2"
18 | )
19 |
20 |
21 | calculateArea : Shape -> Float
22 | calculateArea shape =
23 | case shape of
24 | Point ->
25 | 0
26 |
27 | Square side ->
28 | side * side
29 |
30 | Rectangle width ->
31 | width
32 |
--------------------------------------------------------------------------------
/elm.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "application",
3 | "source-directories": [
4 | "src"
5 | ],
6 | "elm-version": "0.19.1",
7 | "dependencies": {
8 | "direct": {
9 | "elm/browser": "1.0.2",
10 | "elm/core": "1.0.5",
11 | "elm/html": "1.0.0",
12 | "elm/json": "1.1.3",
13 | "elm/random": "1.0.0",
14 | "elm/svg": "1.0.1",
15 | "elm/time": "1.0.0",
16 | "elm-community/random-extra": "3.2.0",
17 | "mpizenberg/elm-pointer-events": "4.0.2"
18 | },
19 | "indirect": {
20 | "elm/bytes": "1.0.8",
21 | "elm/file": "1.0.5",
22 | "elm/url": "1.0.0",
23 | "elm/virtual-dom": "1.0.2"
24 | }
25 | },
26 | "test-dependencies": {
27 | "direct": {},
28 | "indirect": {}
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Exercise020TEA.elm:
--------------------------------------------------------------------------------
1 | module Exercise020TEA exposing (main)
2 |
3 | import Browser
4 | import Html exposing (Html, button, div, span, text)
5 | import Html.Attributes exposing (style)
6 | import Html.Events exposing (onClick)
7 |
8 |
9 | type alias Model =
10 | { count : Int }
11 |
12 |
13 | initialModel : Model
14 | initialModel =
15 | {}
16 |
17 |
18 | type Msg
19 | = Increment
20 |
21 |
22 | update : Msg -> Model -> Model
23 | update msg model =
24 | case msg of
25 | Increment ->
26 | { model | count = model.count + 1 }
27 |
28 |
29 | view : Model -> Html Msg
30 | view model =
31 | div [ style "padding" "1rem" ]
32 | [ span [] [ text (String.fromInt model.count) ]
33 | , text " "
34 | , button [ onClick Increment ] [ text "+1" ]
35 | ]
36 |
37 |
38 | main : Program () Model Msg
39 | main =
40 | Browser.sandbox
41 | { init = initialModel
42 | , view = view
43 | , update = update
44 | }
45 |
--------------------------------------------------------------------------------
/src/Exercise021CounterReset.elm:
--------------------------------------------------------------------------------
1 | module Exercise021CounterReset exposing (..)
2 |
3 | import Browser
4 | import Html exposing (Html, button, div, span, text)
5 | import Html.Attributes exposing (style)
6 | import Html.Events exposing (onClick)
7 |
8 |
9 | type alias Model =
10 | { count : Int }
11 |
12 |
13 | initialModel : Model
14 | initialModel =
15 | { count = 0 }
16 |
17 |
18 | type Msg
19 | = Increment
20 |
21 |
22 | update : Msg -> Model -> Model
23 | update msg model =
24 | case msg of
25 | Increment ->
26 | { model | count = model.count + 1 }
27 |
28 |
29 | view : Model -> Html Msg
30 | view model =
31 | div [ style "padding" "1rem" ]
32 | [ span [] [ text (String.fromInt model.count) ]
33 | , text " "
34 | , button [ onClick Increment ] [ text "+1" ]
35 | , button [ onClick Reset ] [ text "Reset to 0" ]
36 | ]
37 |
38 |
39 | main : Program () Model Msg
40 | main =
41 | Browser.sandbox
42 | { init = initialModel
43 | , view = view
44 | , update = update
45 | }
46 |
--------------------------------------------------------------------------------
/src/Exercise022ListRepeat.elm:
--------------------------------------------------------------------------------
1 | module Exercise022ListRepeat exposing (..)
2 |
3 | import Browser
4 | import Html exposing (Html, button, div, img, span, text)
5 | import Html.Attributes exposing (src, style)
6 | import Html.Events exposing (onClick)
7 |
8 |
9 | type alias Model =
10 | { count : Int }
11 |
12 |
13 | initialModel : Model
14 | initialModel =
15 | { count = 1 }
16 |
17 |
18 | type Msg
19 | = Increment
20 |
21 |
22 | update : Msg -> Model -> Model
23 | update msg model =
24 | case msg of
25 | Increment ->
26 | { model | count = model.count + 1 }
27 |
28 |
29 | view : Model -> Html Msg
30 | view model =
31 | -- There are two compilation errors here...
32 | -- Something tells me the second mistake might be the most useful one...
33 | div [ style "padding" "1rem" ]
34 | [ button [ onClick Increment, style "margin-bottom" "1em" ] [ text "Add a match" ]
35 | , div [] (List.repeat image model.count)
36 | ]
37 |
38 |
39 | image =
40 | img [ src "https://freesvg.org/img/1577808279match.png", style "width" "30px" ] []
41 |
42 |
43 | main : Program () Model Msg
44 | main =
45 | Browser.sandbox
46 | { init = initialModel
47 | , view = view
48 | , update = update
49 | }
50 |
--------------------------------------------------------------------------------
/resources/elm-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
40 |
--------------------------------------------------------------------------------
/src/Exercise023ListMap.elm:
--------------------------------------------------------------------------------
1 | module Exercise023ListMap exposing (..)
2 |
3 | import Browser
4 | import Html exposing (Html, button, div, img, text)
5 | import Html.Attributes exposing (src, style)
6 | import Html.Events exposing (onClick)
7 |
8 |
9 | type alias Model =
10 | { shapes : List Shape }
11 |
12 |
13 | type Shape
14 | = Square Float
15 | | Circle Float
16 |
17 |
18 | initialModel : Model
19 | initialModel =
20 | { shapes = [ Square 50 ] }
21 |
22 |
23 | type Msg
24 | = AddShape Shape
25 |
26 |
27 | update : Msg -> Model -> Model
28 | update msg model =
29 | case msg of
30 | AddShape shape ->
31 | { model | shapes = shape ++ model.shapes }
32 |
33 |
34 | view : Model -> Html Msg
35 | view model =
36 | div []
37 | [ div []
38 | [ button [ onClick (AddShape (Square 50)), style "margin-right" "1em" ] [ text "Add a square" ]
39 | , button [ onClick (AddShape (Circle 50)) ] [ text "Add a circle" ]
40 | ]
41 | , div [ style "padding" "1rem", style "display" "flex" ]
42 | (List.map viewShape)
43 | ]
44 |
45 |
46 | viewShape : Shape -> Html Msg
47 | viewShape shape =
48 | case shape of
49 | Square side ->
50 | div
51 | [ style "width" (floatToPixels side)
52 | , style "height" (floatToPixels side)
53 | , style "background-color" "blue"
54 | , style "margin-right" "1em"
55 | ]
56 | []
57 |
58 | Circle radius ->
59 | div
60 | [ style "width" (floatToPixels radius)
61 | , style "height" (floatToPixels radius)
62 | , style "background-color" "green"
63 | , style "border-radius" "50%"
64 | , style "margin-right" "1em"
65 | ]
66 | []
67 |
68 |
69 | floatToPixels : Float -> String
70 | floatToPixels float =
71 | String.fromFloat float ++ "px"
72 |
73 |
74 | main : Program () Model Msg
75 | main =
76 | Browser.sandbox
77 | { init = initialModel
78 | , view = view
79 | , update = update
80 | }
81 |
--------------------------------------------------------------------------------
/src/Exercise027Time.elm:
--------------------------------------------------------------------------------
1 | module Exercise027Time exposing (..)
2 |
3 | import Browser
4 | import Html exposing (..)
5 | import Task
6 | import Time
7 |
8 |
9 |
10 | -- We want to display the current time. To do this, we need to "subscribe" to the current time:
11 | -- the runtime will regularly send messages containing the current time (in the form of a
12 | -- timestamp/Posix).
13 | --
14 | -- Once again, this protects us from the outside world 😷.
15 |
16 |
17 | type alias Model =
18 | { zone : Time.Zone
19 | , time : Time.Posix
20 | }
21 |
22 |
23 | init : () -> ( Model, Cmd Msg )
24 | init _ =
25 | ( { zone = Time.utc, time = Time.millisToPosix 0 }
26 | , -- Elm forces us to handle the time zone separately from the time itself, which helps us avoid
27 | -- many common pitfalls related to time handling (see https://gist.github.com/timvisee/fcda9bbdff88d45cc9061606b4b923ca).
28 | --
29 | -- This command retrieves the user's time zone:
30 | Task.perform TimeZoneReceived Time.here
31 | )
32 |
33 |
34 | type Msg
35 | = Tick Time.Posix
36 | | TimeZoneReceived Time.Zone
37 |
38 |
39 | update : Msg -> Model -> ( Model, Cmd Msg )
40 | update msg model =
41 | case msg of
42 | Tick newTime ->
43 | ( { model | time = newTime }
44 | , Cmd.none
45 | )
46 |
47 | TimeZoneReceived newZone ->
48 | ( { model | zone = newZone }
49 | , Cmd.none
50 | )
51 |
52 |
53 | subscriptions : Model -> Sub Msg
54 | subscriptions model =
55 | -- But how do we specify that we want to generate a `Tick` message every 1000 milliseconds?
56 | Time.every 1000
57 |
58 |
59 | view : Model -> Html Msg
60 | view model =
61 | let
62 | hour =
63 | String.fromInt (Time.toHour model.zone model.time)
64 |
65 | second =
66 | String.fromInt (Time.toSecond model.zone model.time)
67 | in
68 | h1 [] [ text (hour ++ ":" ++ minute ++ ":" ++ second) ]
69 |
70 |
71 | main =
72 | Browser.element
73 | { init = init
74 | , view = view
75 | , update = update
76 | , subscriptions = subscriptions
77 | }
78 |
--------------------------------------------------------------------------------
/src/Exercise024ListPatternMatching.elm:
--------------------------------------------------------------------------------
1 | module Exercise024ListPatternMatching exposing (..)
2 |
3 | import Browser
4 | import Html exposing (Html, button, div, img, text)
5 | import Html.Attributes exposing (src, style)
6 | import Html.Events exposing (onClick)
7 |
8 |
9 | type alias Model =
10 | { shapes : List Shape }
11 |
12 |
13 | type Shape
14 | = Square Float
15 | | Circle Float
16 |
17 |
18 | initialModel : Model
19 | initialModel =
20 | { shapes = [ Square 50 ] }
21 |
22 |
23 | type Msg
24 | = AddShape Shape
25 | | RemoveShape
26 |
27 |
28 | update : Msg -> Model -> Model
29 | update msg model =
30 | case msg of
31 | AddShape shape ->
32 | { model | shapes = shape :: model.shapes }
33 |
34 | RemoveShape ->
35 | case model.shapes of
36 | firstShape :: otherShapes ->
37 | { model | shapes = otherShapes }
38 |
39 |
40 | view : Model -> Html Msg
41 | view model =
42 | div []
43 | [ div []
44 | [ button [ onClick (AddShape (Square 50)), style "margin-right" "1em" ] [ text "Add a square" ]
45 | , button [ onClick (AddShape (Circle 50)), style "margin-right" "1em" ] [ text "Add a circle" ]
46 | , button [ onClick RemoveShape ] [ text "Remove last added shape" ]
47 | ]
48 | , div [ style "padding" "1rem", style "display" "flex" ]
49 | (List.map viewShape model.shapes)
50 | ]
51 |
52 |
53 | viewShape : Shape -> Html Msg
54 | viewShape shape =
55 | case shape of
56 | Square side ->
57 | div
58 | [ style "width" (floatToPixels side)
59 | , style "height" (floatToPixels side)
60 | , style "background-color" "blue"
61 | , style "margin-right" "1em"
62 | ]
63 | []
64 |
65 | Circle radius ->
66 | div
67 | [ style "width" (floatToPixels radius)
68 | , style "height" (floatToPixels radius)
69 | , style "background-color" "green"
70 | , style "border-radius" "50%"
71 | , style "margin-right" "1em"
72 | ]
73 | []
74 |
75 |
76 | floatToPixels : Float -> String
77 | floatToPixels float =
78 | String.fromFloat float ++ "px"
79 |
80 |
81 | main : Program () Model Msg
82 | main =
83 | Browser.sandbox
84 | { init = initialModel
85 | , view = view
86 | , update = update
87 | }
88 |
--------------------------------------------------------------------------------
/src/Advanced/README.md:
--------------------------------------------------------------------------------
1 | # Advanced Exercises
2 |
3 | Already here? You were quick! Congratulations, you've completed the first part!
4 | Now it's time for you to dive into more complex applications.
5 |
6 | This time, the code compiles, so you can run the app to understand its logic.
7 |
8 | Then, you can start modifying the code by intentionally causing a compilation error, which will let the compiler guide you.
9 | This is a common strategy in Elm.
10 |
11 | ## Draw Me an Elm Tree
12 |
13 | The first application is in the `Advanced/ExerciseDraw.elm` file.
14 | You can draw segments by clicking on two points in the frame (we'll be competing with Photoshop soon!).
15 |
16 | ### Undo the Last Action
17 |
18 | Let's implement a button to undo the last line drawn!
19 |
20 | You can add the following button and let the compiler guide you:
21 |
22 | ```elm
23 | Html.button
24 | [ onClick Cancel
25 | , style "font-size" "20pt"
26 | , style "margin" "5px"
27 | ]
28 | [ Html.text "Undo" ]
29 | ```
30 |
31 | ### Replay the Last Action Undone
32 |
33 | If the user has undone any lines, display an "Redo" button. When clicked, the last undone line is redrawn.
34 |
35 | _Hint:_ You’ll probably need to add a list of undone lines in the `Model`...
36 |
37 | **Warning**: If the user undoes a line and then draws another, the undone line is "lost" and can't be redone.
38 |
39 |
40 | ## Memory Game
41 |
42 | The second application is in the `Advanced/ExerciseMemoryGame.elm` file.
43 | It’s a memory game where you have to find pairs of animals.
44 |
45 | We ask you to implement two new features for this application, one that is fairly simple
46 | and the other one that is much more complex.
47 |
48 | ### Play again
49 |
50 | Once the game is over, we would like to be able to start a new game.
51 | We suggest adding the following button, which should only appear once the game is finished:
52 |
53 | ```elm
54 | Html.button
55 | [ onClick Restart
56 | , style "font-size" "20pt"
57 | , style "margin" "5px"
58 | ]
59 | [ Html.text "Play again" ]
60 | ```
61 |
62 | Now, you should have a compilation error... Let the compiler guide you 😉!
63 |
64 | ### Bomb!
65 |
66 | We want to add a bomb below one of the cards: when the player clicks on it for the first time, the bomb is primed.
67 | If the player clicks on it a second time, the game is lost!
68 |
69 | Here is the "bomb" emoji you can copy/paste: 💣
70 |
71 | We suggest modifying the `Card` type like this:
72 |
73 | ```elm
74 | type Card
75 | = Card Emoji Instance
76 | | Bomb -- Unprimed bomb
77 | | PrimedBomb -- Primed bomb
78 | ```
79 |
80 | And once again, let the compiler guide you!
--------------------------------------------------------------------------------
/src/Exercise025Http.elm:
--------------------------------------------------------------------------------
1 | module Exercise025Http exposing (..)
2 |
3 | import Browser
4 | import Html exposing (Html, button, div, img, pre, text)
5 | import Html.Attributes exposing (src, style)
6 | import Html.Events exposing (onClick)
7 | import Http
8 |
9 |
10 |
11 | -- Until now, our programs have been fairly simple and didn't depend on the outside world.
12 | --
13 | -- In Elm, the outside world is considered "dangerous": what happens when a network request
14 | -- fails? Or returns an unexpected format?
15 | --
16 | -- So we delegate this task to the _runtime_, which protects us and forces us to handle such errors
17 | -- (in Elm, protective measures are always on 😷).
18 | --
19 | -- To perform an HTTP request, we use the concept of a "command": our update function now returns
20 | -- the new model AND a command to execute (in our case, an HTTP request).
21 | --
22 | -- The runtime performs the request, then returns the result to us in a message (in our case, `QuoteFetched`).
23 |
24 |
25 | type alias Model =
26 | { quote : String }
27 |
28 |
29 | initialModel : Model
30 | initialModel =
31 | { quote = "Click on any button to load a quote 😉" }
32 |
33 |
34 | type Msg
35 | = QuoteButtonClicked String
36 | -- The HTTP request can fail, which is why we receive a `Result` that contains either an error (`Http.Error`) or a quote (`String`).
37 | | QuoteFetched (Result Http.Error String)
38 |
39 |
40 | update : Msg -> Model -> ( Model, Cmd Msg )
41 | update msg model =
42 | case msg of
43 | QuoteButtonClicked url ->
44 | ( { model | quote = "Loading..." }, Http.get { expect = Http.expectString QuoteFetched } )
45 |
46 | QuoteFetched result ->
47 | case result of
48 | Err error ->
49 | ( { model | quote = "Error! 😱" }, Cmd.none )
50 |
51 |
52 | view : Model -> Html Msg
53 | view model =
54 | div []
55 | [ div []
56 | [ button [ onClick (QuoteButtonClicked "/resources/quote-1.txt"), style "margin-right" "1em" ] [ text "Get quote 1" ]
57 | , button [ onClick (QuoteButtonClicked "/resources/quote-2.txt"), style "margin-right" "1em" ] [ text "Get quote 2" ]
58 | , button [ onClick (QuoteButtonClicked "/resources/quote-3.txt") ] [ text "Get quote 3" ]
59 | ]
60 | , pre
61 | [ style "padding" "10px"
62 | , style "border" "1px solid gray"
63 | , style "max-width" "500px"
64 | , style "white-space" "pre-wrap"
65 | , style "margin" "10px"
66 | ]
67 | [ text model.quote ]
68 | ]
69 |
70 |
71 | main : Program () Model Msg
72 | main =
73 | Browser.element
74 | { init = \_ -> ( initialModel, Cmd.none )
75 | , view = view
76 | , update = update
77 | , subscriptions = \_ -> Sub.none
78 | }
79 |
--------------------------------------------------------------------------------
/src/Advanced/ExerciseDraw.elm:
--------------------------------------------------------------------------------
1 | module Advanced.ExerciseDraw exposing (..)
2 |
3 | import Browser
4 | import Html exposing (Html, button, div, h1, img, p, pre, text)
5 | import Html.Attributes exposing (height, src, style, width)
6 | import Html.Events exposing (on)
7 | import Html.Events.Extra.Mouse as Mouse
8 | import Json.Decode exposing (Decoder)
9 | import Svg exposing (Svg, circle, line, svg)
10 | import Svg.Attributes exposing (cx, cy, r, x1, x2, y1, y2)
11 |
12 |
13 | type alias Point =
14 | { x : Float, y : Float }
15 |
16 |
17 | type alias Line =
18 | { from : Point, to : Point }
19 |
20 |
21 | type alias Model =
22 | { lines : List Line
23 | , firstPointForNextLine : Maybe Point
24 | }
25 |
26 |
27 | initialModel : Model
28 | initialModel =
29 | { lines = []
30 | , firstPointForNextLine = Nothing
31 | }
32 |
33 |
34 | type Msg
35 | = CanvasClickedAt Point
36 |
37 |
38 | update : Msg -> Model -> Model
39 | update msg model =
40 | case msg of
41 | CanvasClickedAt pointClicked ->
42 | case model.firstPointForNextLine of
43 | Just firstPoint ->
44 | { model
45 | | firstPointForNextLine = Nothing
46 | , lines = { from = firstPoint, to = pointClicked } :: model.lines
47 | }
48 |
49 | Nothing ->
50 | { model | firstPointForNextLine = Just pointClicked }
51 |
52 |
53 | view : Model -> Html Msg
54 | view model =
55 | div [ style "padding" "1rem" ]
56 | [ h1 [] [ text "Draw lines!" ]
57 | , p [] [ text "Click at different spots in the frame below to draw lines." ]
58 | , svg
59 | [ style "border" "1px black solid"
60 | , width 800
61 | , height 400
62 | , Mouse.onClick
63 | (\event ->
64 | CanvasClickedAt
65 | { x = Tuple.first event.offsetPos
66 | , y = Tuple.second event.offsetPos
67 | }
68 | )
69 | ]
70 | (drawFirstPoint model.firstPointForNextLine :: List.map drawLine model.lines)
71 | ]
72 |
73 |
74 | drawLine : Line -> Svg Msg
75 | drawLine { from, to } =
76 | line
77 | [ x1 (String.fromFloat from.x)
78 | , y1 (String.fromFloat from.y)
79 | , x2 (String.fromFloat to.x)
80 | , y2 (String.fromFloat to.y)
81 | , Svg.Attributes.style "stroke:rgb(255,0,0);stroke-width:2"
82 | ]
83 | []
84 |
85 |
86 | drawFirstPoint : Maybe Point -> Svg Msg
87 | drawFirstPoint maybePoint =
88 | case maybePoint of
89 | Just point ->
90 | circle
91 | [ cx (String.fromFloat point.x)
92 | , cy (String.fromFloat point.y)
93 | , r "2"
94 | , Svg.Attributes.style "fill:rgb(255,0,0)"
95 | ]
96 | []
97 |
98 | Nothing ->
99 | circle [] []
100 |
101 |
102 | main : Program () Model Msg
103 | main =
104 | Browser.sandbox
105 | { init = initialModel
106 | , view = view
107 | , update = update
108 | }
109 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Compiler Driven Development with Elm!
2 |
3 |
4 |
5 |
6 | _🇫🇷 Si vous cherchez une version française de ce workshop, [cliquez ici](https://github.com/jgrenat/elm-compiler-driven-development)._
7 |
8 | Welcome to our workshop to discover the Elm language and a new programming paradigm: let the compiler tell you what to do!
9 |
10 | But first, let's start by installing Elm:
11 |
12 | ## Installing Elm
13 |
14 | There are two ways to install Elm. You can either [download and install it from this page (recommended) of the official guide](https://guide.elm-lang.org/install/elm.html), or use npm with the following command:
15 |
16 | ```bash
17 | npm install --global elm
18 | ```
19 |
20 | ## Set Up Elm Plugin in your Editor
21 |
22 | Now that Elm is installed, you can set up your text editor for syntax highlighting!
23 |
24 | - [Atom](https://atom.io/packages/language-elm)
25 | - [Emacs](https://github.com/jcollard/elm-mode)
26 | - [Webstorm / IntelliJ](https://github.com/klazuka/intellij-elm)
27 | - [Sublime Text](https://guide.elm-lang.org/install/editor.html)
28 | - [Vim](https://github.com/ElmCast/elm-vim)
29 | - [VS Code](https://marketplace.visualstudio.com/items?itemName=Elmtooling.elm-ls-vscode)
30 |
31 | **Note :** This workshop is designed to work with the error messages as shown by the compiler. The messages displayed by the plugins listed above may not exactly match the compiler’s errors.
32 | We therefore recommend "ignoring" the errors shown in your editor _for this workshop_ and instead focusing on those displayed in your browser.
33 |
34 |
35 | ## Get the workshop
36 |
37 | You can now get the code for this workshop either by [downloading the archive](https://github.com/jgrenat/elm-compiler-driven-development/archive/master.zip), or by cloning it:
38 |
39 | ```bash
40 | git clone https://github.com/jgrenat/elm-compiler-driven-development-en.git
41 | cd elm-compiler-driven-development-en
42 | ```
43 |
44 | Once you have the workshop files using one of the two methods above, run the following command at the root of the project:
45 |
46 | ```bash
47 | elm reactor
48 | ```
49 |
50 | The exercises files can be accessed at http://localhost:8000/src
51 |
52 | ## Workshop Flow
53 |
54 | Each file represents an exercise. You can begin an exercise by opening it in Elm Reactor, for example at
55 | http://localhost:8000/src/Exercise010String.elm for the first one.
56 |
57 | Don’t be surprised if an error appears — that’s normal! Read it carefully; the compiler is your mentor to help you fix it! 💪
58 |
59 | Once the code compiles, the exercise is complete. Congrats! You can move on to the next one! 🎉
60 |
61 | After finishing the first exercises, you can move to the next level in the `Advanced` folder.
62 |
63 | You’ll follow the same path to develop new features in an existing application: write code that triggers a compilation error and let the compiler guide you. This is a very effective approach, no matter the size of your project.
64 | This is what's called Compiler-Driven Development!
65 |
66 | ## Stuck?
67 |
68 | No worry — we’re here to help! Wave your hand wildly and we’ll come to you! 🙋♀️🙋♂️
69 |
70 |
71 | ## Authors
72 |
73 | This workshop was authored by [Sébastien Besnier](https://github.com/sebsheep) and [Jordane Grenat](https://github.com/jgrenat).
74 | Feel free to use it for yourself or to teach others! Any feedback is welcome!
75 |
--------------------------------------------------------------------------------
/src/Exercise026HttpDecoder.elm:
--------------------------------------------------------------------------------
1 | module Exercise026HttpDecoder exposing (..)
2 |
3 | import Browser
4 | import Html exposing (..)
5 | import Html.Attributes exposing (..)
6 | import Html.Events exposing (..)
7 | import Http
8 | import Json.Decode exposing (Decoder)
9 |
10 |
11 | type Model
12 | = Failure String
13 | | Loading
14 | | Success Cat
15 |
16 |
17 | type alias Cat =
18 | { title : String
19 | , url : String
20 | }
21 |
22 |
23 | init : () -> ( Model, Cmd Msg )
24 | init _ =
25 | ( Loading, getRandomCatGif )
26 |
27 |
28 | type Msg
29 | = CatButtonClicked
30 | | GifReceived (Result Http.Error Cat)
31 |
32 |
33 | update : Msg -> Model -> ( Model, Cmd Msg )
34 | update msg model =
35 | case msg of
36 | CatButtonClicked ->
37 | ( Loading, getRandomCatGif )
38 |
39 | GifReceived result ->
40 | case result of
41 | Ok cat ->
42 | ( Success cat, Cmd.none )
43 |
44 | Err error ->
45 | case error of
46 | Http.BadBody errorMsg ->
47 | ( Failure errorMsg, Cmd.none )
48 |
49 | _ ->
50 | ( Failure "Http error!", Cmd.none )
51 |
52 |
53 |
54 | -- VIEW
55 |
56 |
57 | view : Model -> Html Msg
58 | view model =
59 | div []
60 | [ h2 [] [ text "Random Cats" ]
61 | , viewGif model
62 | ]
63 |
64 |
65 | viewGif : Model -> Html Msg
66 | viewGif model =
67 | case model of
68 | Failure errorMsg ->
69 | div []
70 | [ text ("An error occurred: " ++ errorMsg)
71 | , button [ onClick CatButtonClicked ] [ text "Try again!" ]
72 | ]
73 |
74 | Loading ->
75 | text "Loading..."
76 |
77 | Success cat ->
78 | div []
79 | [ h1 [] [ text cat.title ]
80 | , button [ onClick CatButtonClicked, style "display" "block" ] [ text "Another one!" ]
81 | , img [ src cat.url ] []
82 | ]
83 |
84 |
85 | getRandomCatGif : Cmd Msg
86 | getRandomCatGif =
87 | Http.get
88 | { url = "https://api.giphy.com/v1/gifs/random?api_key=kOZdCy0KDR2n8Y83kawP0zdqUMqpHYRj&tag=cat"
89 | , expect = Http.expectJson GifReceived gifDecoder
90 | }
91 |
92 |
93 | gifDecoder : Decoder Cat
94 | gifDecoder =
95 | -- Elm can't guess the shape of the JSON we receive, so we need to tell it which fields
96 | -- we're interested in using this decoder.
97 | --
98 | -- The JSON looks like this:
99 | -- {
100 | -- "data": {
101 | -- "title": "Tired cat",
102 | -- "images" : { "original": { "url" : "http://...", ...}, ...},
103 | -- ...,
104 | -- },
105 | -- ...,
106 | -- }
107 | --
108 | -- You can see the full structure at the following link:
109 | -- https://api.giphy.com/v1/gifs/random?api_key=kOZdCy0KDR2n8Y83kawP0zdqUMqpHYRj&tag=cat
110 | Json.Decode.map2 Cat
111 | (Json.Decode.at [ "data", "title" ] Json.Decode.int)
112 | (Json.Decode.at [ "data", "images", "original", "url" ] Json.Decode.string)
113 |
114 |
115 | main =
116 | Browser.element
117 | { init = init
118 | , update = update
119 | , subscriptions = \_ -> Sub.none
120 | , view = view
121 | }
122 |
123 |
124 |
125 | -- Inspired by:
126 | -- https://elm-lang.org/examples/cat-gifs
127 |
--------------------------------------------------------------------------------
/src/Advanced/ExerciseMemoryGame.elm:
--------------------------------------------------------------------------------
1 | module Advanced.ExerciseMemoryGame exposing (..)
2 |
3 | import Browser
4 | import Html exposing (Html)
5 | import Html.Attributes exposing (disabled, style)
6 | import Html.Events exposing (onClick)
7 | import Random
8 | import Random.List as Random
9 | import Time
10 |
11 |
12 | type alias Model =
13 | { state : State
14 | , cards : Maybe (List Card)
15 | , matched : List Card
16 | }
17 |
18 |
19 | type State
20 | = Hidden
21 | | OneRevealed Card
22 | | TwoRevealed Card Card
23 | | Solved
24 |
25 |
26 | type Card
27 | = Card Emoji Instance
28 |
29 |
30 | type Instance
31 | = A
32 | | B
33 |
34 |
35 | type Msg
36 | = Click Card
37 | | TimeOut
38 | | NewGame (List Card)
39 |
40 |
41 | main : Program () Model Msg
42 | main =
43 | Browser.element
44 | { init = init
45 | , update = update
46 | , subscriptions = subscriptions
47 | , view = view
48 | }
49 |
50 |
51 | init : () -> ( Model, Cmd Msg )
52 | init () =
53 | ( initialModel, newGame numPairsInit )
54 |
55 |
56 | numPairsInit : Int
57 | numPairsInit =
58 | 3
59 |
60 |
61 | initialModel : Model
62 | initialModel =
63 | { state = Hidden
64 | , cards = Nothing
65 | , matched = []
66 | }
67 |
68 |
69 | newGame : Int -> Cmd Msg
70 | newGame numPairs =
71 | Random.generate
72 | NewGame
73 | (Random.shuffle emojisList
74 | |> Random.andThen
75 | (\code ->
76 | createCards code numPairs
77 | |> Random.shuffle
78 | )
79 | )
80 |
81 |
82 | createCards : List Emoji -> Int -> List Card
83 | createCards emojis numPairs =
84 | List.take numPairs emojis
85 | |> List.concatMap (\emoji -> [ Card emoji A, Card emoji B ])
86 |
87 |
88 | update : Msg -> Model -> ( Model, Cmd Msg )
89 | update msg model =
90 | case msg of
91 | TimeOut ->
92 | ( case model.state of
93 | TwoRevealed _ _ ->
94 | { model | state = Hidden }
95 |
96 | _ ->
97 | model
98 | , Cmd.none
99 | )
100 |
101 | Click card ->
102 | ( if List.member card model.matched then
103 | model
104 |
105 | else
106 | case model.state of
107 | Hidden ->
108 | { model | state = OneRevealed card }
109 |
110 | OneRevealed card1 ->
111 | revealAnother model card1 card
112 |
113 | _ ->
114 | model
115 | , Cmd.none
116 | )
117 |
118 | NewGame cards ->
119 | ( { model | cards = Just cards }
120 | , Cmd.none
121 | )
122 |
123 |
124 | revealAnother : Model -> Card -> Card -> Model
125 | revealAnother model alreadyRevealed toReveal =
126 | if toReveal == alreadyRevealed then
127 | model
128 |
129 | else
130 | let
131 | matched : List Card
132 | matched =
133 | if matching numPairsInit alreadyRevealed toReveal then
134 | alreadyRevealed :: toReveal :: model.matched
135 |
136 | else
137 | model.matched
138 | in
139 | { model
140 | | state =
141 | if List.length matched == numPairsInit * 2 then
142 | Solved
143 |
144 | else
145 | TwoRevealed alreadyRevealed toReveal
146 | , matched = matched
147 | }
148 |
149 |
150 | matching : Int -> Card -> Card -> Bool
151 | matching numPairs card1 card2 =
152 | case ( card1, card2 ) of
153 | ( Card index1 _, Card index2 _ ) ->
154 | index1 == index2
155 |
156 |
157 | subscriptions : Model -> Sub Msg
158 | subscriptions model =
159 | case model.state of
160 | TwoRevealed _ _ ->
161 | Time.every 1000 (always TimeOut)
162 |
163 | _ ->
164 | Sub.none
165 |
166 |
167 | view : Model -> Html Msg
168 | view model =
169 | case model.cards of
170 | Just cards ->
171 | let
172 | numCards : Int
173 | numCards =
174 | List.length cards
175 |
176 | columns : Int
177 | columns =
178 | numColumns numCards
179 |
180 | rows : Int
181 | rows =
182 | numCards // columns
183 | in
184 | Html.div []
185 | [ header model
186 | , Html.div
187 | (grid rows columns)
188 | (List.map
189 | (cardView model.matched model.state)
190 | cards
191 | )
192 | ]
193 |
194 | Nothing ->
195 | Html.span messageStyle [ Html.text "Shuffling …" ]
196 |
197 |
198 | {-| Try for equal number of rows and columns,
199 | favoring more columns if numCards is not a perfect square
200 | -}
201 | numColumns : Int -> Int
202 | numColumns numCards =
203 | Maybe.withDefault numCards
204 | (List.filter
205 | (\n -> modBy n numCards == 0)
206 | (List.range
207 | (numCards
208 | |> toFloat
209 | |> sqrt
210 | |> ceiling
211 | )
212 | numCards
213 | )
214 | |> List.head
215 | )
216 |
217 |
218 | header : Model -> Html Msg
219 | header model =
220 | Html.div [ style "padding" "10px" ]
221 | (case model.state of
222 | Solved ->
223 | [ Html.span messageStyle [ Html.text "Congrats!" ]
224 | , Html.div []
225 | [ Html.span messageStyle
226 | [ Html.text "Play again?" ]
227 | ]
228 | ]
229 |
230 | TwoRevealed card1 card2 ->
231 | [ Html.span messageStyle
232 | [ Html.text
233 | (if matching numPairsInit card1 card2 then
234 | "Pair revealed!"
235 |
236 | else
237 | "It's not a pair, try again."
238 | )
239 | ]
240 | ]
241 |
242 | _ ->
243 | [ Html.span messageStyle [ Html.text "Click on cards to reveal them" ] ]
244 | )
245 |
246 |
247 | cardView : List Card -> State -> Card -> Html Msg
248 | cardView matched state card =
249 | if List.member card matched then
250 | cardRevealedView card
251 |
252 | else
253 | case state of
254 | OneRevealed card1 ->
255 | if card == card1 then
256 | cardRevealedView card
257 |
258 | else
259 | cardHiddenView matched state card
260 |
261 | TwoRevealed card1 card2 ->
262 | if List.member card [ card1, card2 ] then
263 | cardRevealedView card
264 |
265 | else
266 | cardHiddenView matched state card
267 |
268 | _ ->
269 | cardHiddenView matched state card
270 |
271 |
272 | cardRevealedView : Card -> Html Msg
273 | cardRevealedView card =
274 | Html.span cardStyle
275 | [ case card of
276 | Card emoji _ ->
277 | emojiToString emoji
278 | |> Html.text
279 | ]
280 |
281 |
282 | cardHiddenView : List Card -> State -> Card -> Html Msg
283 | cardHiddenView matched state card =
284 | let
285 | isDisabled =
286 | List.member card matched
287 | || (case state of
288 | TwoRevealed _ _ ->
289 | True
290 |
291 | _ ->
292 | False
293 | )
294 | in
295 | Html.button
296 | (onClick (Click card)
297 | :: disabled isDisabled
298 | :: cardStyle
299 | )
300 | [ Html.text "❓" ]
301 |
302 |
303 | grid : Int -> Int -> List (Html.Attribute Msg)
304 | grid rows columns =
305 | [ style "display" "grid"
306 | , style "grid-template-columns" (String.join " " (List.repeat columns "60pt"))
307 | , style "grid-template-rows" (String.join " " (List.repeat rows "60pt"))
308 | ]
309 |
310 |
311 | cardStyle : List (Html.Attribute Msg)
312 | cardStyle =
313 | [ style "font-size" "40pt"
314 | , style "margin" "5px"
315 | , style "padding" "2px"
316 | , style "border-radius" "1px"
317 | ]
318 |
319 |
320 | messageStyle : List (Html.Attribute Msg)
321 | messageStyle =
322 | [ style "font-size" "20pt"
323 | , style "margin" "5px"
324 | , style "padding" "2px"
325 | ]
326 |
327 |
328 |
329 | -- HANDLING EMOJIS
330 |
331 |
332 | type Emoji
333 | = Emoji Int
334 |
335 |
336 | emojiToString : Emoji -> String
337 | emojiToString (Emoji code) =
338 | code
339 | |> Char.fromCode
340 | |> String.fromChar
341 |
342 |
343 | emojisList : List Emoji
344 | emojisList =
345 | List.map Emoji
346 | [ 0x0001F400
347 | , 0x0001F403
348 | , 0x0001F404
349 | , 0x0001F405
350 | , 0x0001F406
351 | , 0x0001F407
352 | , 0x0001F408
353 | , 0x0001F409
354 | , 0x0001F40A
355 | ]
356 |
357 |
358 |
359 | -- Inspired by https://github.com/O-O-Balance/pairs/
360 |
--------------------------------------------------------------------------------