├── .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 | 6 | 7 | 10 | 11 | 14 | 15 | 22 | 23 | 26 | 27 | 30 | 31 | 34 | 35 | 38 | 39 | 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 | --------------------------------------------------------------------------------