From 3c8bfe85c9fb6dfa064cb1fc5bcc93af46cdd49f Mon Sep 17 00:00:00 2001 From: William Carroll Date: Sat, 11 Apr 2020 16:50:02 +0100 Subject: [PATCH] Prefer type alias to type Elm reminds me of Haskell. In fact, I'm using `haskell-mode` (for now) in Emacs to write my Elm code, and it works reliably. I'm not writing a Haskell app, but if I were, I would define my application Model with the following Haskell code: ```haskell data Model = Model { whitelistedChords :: [Theory.Chord] , selectedChord :: Theory.Chord , isPaused :: Bool , tempo :: Int } ``` When I first modelled my application state, I did something similar. After reading more Elm examples of SPAs, I see that people prefer using type aliases to define records. As far as I know, you cannot do this in Haskell; I believe all types are "tagged" (something about "nominal typing" comes to mind). Anyhow, Elm isn't Haskell; Haskell has cool features like type classes; Elm has cool features like human-readable error messages and exhaustiveness checking for cases. I love Haskell, and I love Elm, and you didn't ask. Anyhow, this commit refactors my records as type aliases instead of types. I think the resulting code is more readable and ergonomic. --- .../sandbox/chord-drill-sergeant/src/Main.elm | 81 +++++++------------ .../chord-drill-sergeant/src/Theory.elm | 27 ++++--- 2 files changed, 48 insertions(+), 60 deletions(-) diff --git a/website/sandbox/chord-drill-sergeant/src/Main.elm b/website/sandbox/chord-drill-sergeant/src/Main.elm index bfa03f629..3f1168b53 100644 --- a/website/sandbox/chord-drill-sergeant/src/Main.elm +++ b/website/sandbox/chord-drill-sergeant/src/Main.elm @@ -11,11 +11,12 @@ import Time exposing (..) import Piano import Theory -type Model = Model { whitelistedChords : List Theory.Chord - , selectedChord : Theory.Chord - , isPaused : Bool - , tempo : Int - } +type alias Model = + { whitelistedChords : List Theory.Chord + , selectedChord : Theory.Chord + , isPaused : Bool + , tempo : Int + } type Msg = NextChord | NewChord Theory.Chord @@ -28,7 +29,7 @@ tempoStep : Int tempoStep = 100 viewChord : Theory.Chord -> String -viewChord (Theory.Chord (note, chordType, chordPosition)) = +viewChord {note, chordType, chordPosition} = viewNote note ++ " " ++ (case chordType of Theory.Major -> "major" @@ -65,19 +66,23 @@ viewNote note = Theory.B -> "B" cmajor : Theory.Chord -cmajor = Theory.Chord (Theory.C, Theory.Major, Theory.First) +cmajor = + { note = Theory.C + , chordType = Theory.Major + , chordPosition = Theory.First + } {-| The initial state for the application. -} init : Model init = - Model { whitelistedChords = Theory.allChords - , selectedChord = cmajor - , isPaused = True - , tempo = 1000 - } + { whitelistedChords = Theory.allChords + , selectedChord = cmajor + , isPaused = True + , tempo = 1000 + } subscriptions : Model -> Sub Msg -subscriptions (Model {isPaused, tempo}) = +subscriptions {isPaused, tempo} = if isPaused then Sub.none else @@ -85,71 +90,47 @@ subscriptions (Model {isPaused, tempo}) = {-| Now that we have state, we need a function to change the state. -} update : Msg -> Model -> (Model, Cmd Msg) -update msg (Model {whitelistedChords, selectedChord, isPaused, tempo}) = +update msg model = case msg of - NewChord chord -> ( Model { whitelistedChords = whitelistedChords - , selectedChord = chord - , isPaused = isPaused - , tempo = tempo - } + NewChord chord -> ( { model | selectedChord = chord } , Cmd.none ) - NextChord -> ( Model { whitelistedChords = whitelistedChords - , selectedChord = selectedChord - , isPaused = isPaused - , tempo = tempo - } + NextChord -> ( model , Random.generate (\x -> case x of (Just chord, _) -> NewChord chord (Nothing, _) -> NewChord cmajor) - (Random.List.choose whitelistedChords) + (Random.List.choose model.whitelistedChords) ) - Play -> ( Model { whitelistedChords = whitelistedChords - , selectedChord = selectedChord - , isPaused = False - , tempo = tempo - } + Play -> ( { model | isPaused = False } , Cmd.none ) - Pause -> ( Model { whitelistedChords = whitelistedChords - , selectedChord = selectedChord - , isPaused = True - , tempo = tempo - } + Pause -> ( { model | isPaused = True } , Cmd.none ) - IncreaseTempo -> ( Model { whitelistedChords = whitelistedChords - , selectedChord = selectedChord - , isPaused = isPaused - , tempo = tempo - tempoStep - } + IncreaseTempo -> ( { model | tempo = model.tempo - tempoStep } , Cmd.none ) - DecreaseTempo -> ( Model { whitelistedChords = whitelistedChords - , selectedChord = selectedChord - , isPaused = isPaused - , tempo = tempo + tempoStep - } + DecreaseTempo -> ( { model | tempo = model.tempo + tempoStep } , Cmd.none ) playPause : Model -> Html Msg -playPause (Model {isPaused}) = +playPause {isPaused} = if isPaused then button [ onClick Play ] [ text "Play" ] else button [ onClick Pause ] [ text "Pause" ] view : Model -> Html Msg -view (Model {selectedChord, tempo} as model) = - div [] [ p [] [ text (viewChord selectedChord) ] - , p [] [ text (String.fromInt tempo) ] +view model = + div [] [ p [] [ text (viewChord model.selectedChord) ] + , p [] [ text (String.fromInt model.tempo) ] , button [ onClick NextChord ] [ text "Next Chord" ] , button [ onClick IncreaseTempo ] [ text "Faster" ] , button [ onClick DecreaseTempo ] [ text "Slower" ] , playPause model - , Piano.render { highlight = Theory.notesForChord selectedChord } + , Piano.render { highlight = Theory.notesForChord model.selectedChord } ] {-| For now, I'm just dumping things onto the page to sketch ideas. -} diff --git a/website/sandbox/chord-drill-sergeant/src/Theory.elm b/website/sandbox/chord-drill-sergeant/src/Theory.elm index c80fffc39..4a275fa79 100644 --- a/website/sandbox/chord-drill-sergeant/src/Theory.elm +++ b/website/sandbox/chord-drill-sergeant/src/Theory.elm @@ -36,7 +36,11 @@ type Interval = Half | MinorThird {-| A bundle of notes which are usually, but not necessarily harmonious. -} -type Chord = Chord (Note, ChordType, ChordPosition) +type alias Chord = + { note : Note + , chordType : ChordType + , chordPosition : ChordPosition + } {-| Many possible chords exist. This type encodes the possibilities. I am tempted to model these in a more "DRY" way, but I worry that this abstraction @@ -62,7 +66,10 @@ type ChordPosition = First {-| Songs are written in one or more keys, which define the notes and therefore chords that harmonize with one another. -} -type Key = Key (Note, Mode) +type alias Key = + { note : Note + , mode : Mode + } {-| We create "scales" by enumerating the notes of a given key. These keys are defined by the "tonic" note and the "mode". I thought about including Ionian, @@ -160,16 +167,13 @@ applySteps steps note = {-| Return a list of the notes that comprise a `chord` -} notesForChord : Chord -> List Note -notesForChord chord = - case chord of - -- TODO(wpcarro): Use the Position to rotate the chord n times - Chord (note, chordType, _) -> note :: applySteps (intervalsForChordType chordType) note +notesForChord {note, chordType} = + note :: applySteps (intervalsForChordType chordType) note {-| Return the scale for a given `key` -} notesForKey : Key -> List Note -notesForKey key = - case key of - Key (note, mode) -> applySteps (intervalsForMode mode) note +notesForKey {note, mode} = + applySteps (intervalsForMode mode) note {-| Return a list of all of the chords that we know about. -} allChords : List Chord @@ -206,4 +210,7 @@ allChords = notes |> List.Extra.andThen (\note -> chordTypes |> List.Extra.andThen (\chordType -> chordPositions - |> List.Extra.andThen (\chordPosition -> [Chord (note, chordType, chordPosition)]))) + |> List.Extra.andThen (\chordPosition -> [{ note = note + , chordType = chordType + , chordPosition = chordPosition + }])))