tempusfugit (120) [Avatar] Offline
#1
Page 123(127)
ADDING A <PAPER-SLIDER> TO THE PAGE
TIP Because of partial application, we could also have written these as label = node "label" and input = node "input" - either way works!
I had to smile at this one. Here I would have expected most authors to roll out the "And because in Elm all functions are curried, we could have also ..." but I think the more general notion of (automatic) partial application works well enough. This would also be a good opportunity to sidebar/footnote about/link to "point free style" - though of course that is far from essential.

https://www.elm-tutorial.org/en/tips-tricks/point-free-style.html

Page 124(128)
VIEWING THE SLIDERS
(2) <paper-slider> that goes from 0 to 11
Hmm...
https://en.wikipedia.org/wiki/Up_to_eleven
image

Page 127(131)
USING JSON.DECODE.MAP
Json.Decode.map negate float
Apart from the fact that it seems inconsistent to fully qualify map and not float when they both originate from the same package (Json.Decode), the reader may benefit from a gentle reminder that float(, int and string) are decoders rather than just some kind of type identifier/tag, i.e.

Json.Decode.map negate Json.Decode.float

or alternately some form of
import Json.Decode as D

D.map negate D.float

Listing 5.2 onImmediateValueChange
The type annotations in the let block are a nice touch - lets just hope most readers appreciate that additional information rather than just judging it as more boilerplate.

Page 130(134)
Table 5.4 Updating the model
It is at this point where the (mental) naming conflict between "filter" and for example "List.filter" (or Array, Dict, Set, String) becomes most noticable. Unfortunately "effect" is similarly hampered. Something like "imageFilter" or "imageEffect" may curtail some confusion.

Page 132(136)
CHOOSING RELIABILTY
By using individual fields instead of a list of records, we can rule out the entire category of bugs related to invalid filter names.
Aren't we kind of "throwing the baby out with the bath water" here? The root cause here is that String is permissive enough to allow values that aren't valid filter names - so the solution would be to define a union type
type ImageEffect = Hue | Ripple | Noise
and use that instead of a string. And for good measure we could also use a Dict instead of a List.

Page 134(138)
PORT COMMANDS NEVER SEND MESSAGES
... but for now it’s enough to know that a Cmd msg is a command which produces no message after it completes.
I think this statement is a bit too general and will only create more confusion - for me this restatement makes more sense:
... but for now it’s enough to know that a command which produces no message after it completes is annotated with a type of Cmd msg.
"Cmd msg" is simply a type specification where the type variable is unresolved - the reason Cmd.none can get away with it is because
  • msg matches the type of the messages in an empty list, i.e. messages who's type simply cannot be determined
  • the need to resolve msg never comes up (a situation which also applies to setFilters)

  • In other contexts "Cmd msg" may simply convey that there isn't enough information to completely resolve the type of msg rather than always conveying that "there is no type because no value is being returned" as it is the case with "Cmd.none" and "setFilters".

    Also Fire-and-Forget is an established conversation pattern.

    Page 141(145)
    NOTE This is another example of Elm’s design emphasizing being explicit.
    I find it somewhat ironic that just a few lines earlier the literal "11" was used implicitly as a Float when it really looks like an Int because it is treated as an:
    Int or Float depending on usage
    Presumably this goes back to JavaScript having no specific type for integers.

    Page 148(152)
    5.3.2 Receiving initialization arguments via Flags

    It may be an idea to acknowledge the situation outlined on page 150(154) right up front:
    we’ll see the version number proudly displaying for the briefest of moments before the first photo loads.
    i.e., that the upcoming section primarily exists for its instructional value rather than it's "practical" effect.

    That testing chapter could come in really handy right about now ...



    tempusfugit (120) [Avatar] Offline
    #2
    Well, blow me down
    Hint: Only ints, floats, chars, strings, lists, and tuples are comparable.
    So much for using union types as keys in Dict

    https://github.com/elm-lang/elm-compiler/issues/774

    I guess I got a bit spoiled by the ordering capability in Erlang and Elixir.

    So for a workaround there is typically a little more code required.
    type ImgEffect
        = Hue
        | Ripple
        | Noise
    
    imgEffectString : ImgEffect -> String
    imgEffectString imgEffect =
        case imgEffect of
            Hue ->
                "Hue"
            Ripple ->
                "Ripple"
            Noise ->
                "Noise"
    
    type Msg
        = SelectByUrl String
        | SelectByIndex Int
        | SetStatus String
        | SurpriseMe
        | SetSize ThumbnailSize
        | LoadPhotos (Result Http.Error (List Photo))
        | SetImgEffect ImgEffect Int
    
    type alias Model =
        { photos : List Photo
        , status : String
        , selectedUrl : Maybe String
        , loadingError : Maybe String
        , chosenSize : ThumbnailSize
        , imgEffects : Dict String Int
        }
    
    initialModel : Model
    initialModel =
        { photos = []
        , status = ""
        , selectedUrl = Nothing
        , loadingError = Nothing
        , chosenSize = Medium
        , imgEffects =
            Dict.fromList
                (List.map
                    (\(k,v) -> (imgEffectString(k),v))
                    [(Hue,0),(Ripple,0),(Noise,0)]
                )
        }
    
    insertImgEffect : ImgEffect -> Int -> Dict String Int -> Dict String Int
    insertImgEffect imgEffect magnitude imgEffects =
        Dict.insert (imgEffectString imgEffect) magnitude imgEffects
    
    toFilterConfig : (String,Int) -> { name: String, amount : Float }
    toFilterConfig (name,magnitude) =
        { name = name
        , amount = toFloat magnitude / 11
        }
    
    applyImgEffects : Model -> ( Model, Cmd Msg )
    applyImgEffects model =
        case model.selectedUrl of
            Just selectedUrl ->
                let
                    filters = List.map toFilterConfig <| Dict.toList model.imgEffects
                    url = urlPrefix ++ "large/" ++ selectedUrl
                in
                    ( model, setFilters { url = url, filters = filters } )
    
            Nothing ->
                ( model, Cmd.none )
    
    update : Msg -> Model -> ( Model, Cmd Msg )
    update msg model =
        case msg of
            SetStatus status ->
                ( { model | status = status }, Cmd.none )
    
            SelectByIndex index ->
                let
                    newSelectedUrl : Maybe String
                    newSelectedUrl =
                        model.photos
                            |> Array.fromList
                            |> Array.get index
                            |> Maybe.map .url
                in
                    applyImgEffects { model | selectedUrl = newSelectedUrl }
    
            SelectByUrl selectedUrl ->
                applyImgEffects { model | selectedUrl = Just selectedUrl }
    
            SurpriseMe ->
                let
                    randomPhotoPicker =
                        Random.int 0 (List.length model.photos - 1)
                in
                    ( model, Random.generate SelectByIndex randomPhotoPicker )
    
            SetSize size ->
                ( { model | chosenSize = size }, Cmd.none )
    
            LoadPhotos (Ok photos) ->
                applyImgEffects
                    { model
                        | photos = photos
                        , selectedUrl = Maybe.map .url (List.head photos)
                    }
    
            LoadPhotos (Err _) ->
                ( { model
                    | loadingError = Just "Error! (Try turning it off and on again?)"
                  }
                , Cmd.none
                )
    
            SetImgEffect imgEffect magnitude ->
                applyImgEffects
                    { model
                        | imgEffects = insertImgEffect imgEffect magnitude model.imgEffects
                    }
    
    viewImgEffect : ImgEffect -> Dict String Int -> Html Msg
    viewImgEffect imgEffect effects =
        let
            name = imgEffectString imgEffect
            magnitude = Maybe.withDefault 0 <| Dict.get name effects
            toMsg = SetImgEffect imgEffect
        in
            div [ class "filter-slider" ]
                [ label [] [ text name ]
                , paperSlider [ Attr.max "11", onImmediateValueChange toMsg ] []
                , label [] [ text (toString magnitude) ]
                ]
    
    view : Model -> Html Msg
    view model =
        div [ class "content" ]
            [ h1 [] [ text "Photo Groove" ]
            , button
                [ onClick SurpriseMe ]
                [ text "Surprise Me!" ]
            , div [ class "status" ] [ text model.status ]
            , div [ class "filters" ]
                [ viewImgEffect Hue model.imgEffects
                , viewImgEffect Ripple model.imgEffects
                , viewImgEffect Noise model.imgEffects
                ]
            , h3 [] [ text "Thumbnail Size:" ]
            , div [ id "choose-size" ]
                (List.map viewSizeChooser [ Small, Medium, Large ])
            , div [ id "thumbnails", class (sizeToString model.chosenSize) ]
                (List.map (viewThumbnail model.selectedUrl) model.photos)
            , viewLarge model.selectedUrl
            ]