Skip to content

lue-bird/elm-review-missing-record-field-lens

Repository files navigation

Despite what the name suggests, this package contains multiple elm-review rules to help with automatic code generation based on use:

When lue-bird/generate-elm – a framework for making code generation easy and safe – is finished, every functionality will be ported over.


You find yourself writing code like ↓ ?

... path newInput =
    \state ->
        { state
            | projects =
                state.projects
                    |> Scroll.focusMap
                        (Fillable.map
                            (\project ->
                                { project
                                    | calls =
                                        project.calls
                                            |> List.map
                                                (Tree.elementAlter
                                                    ( path, Tree.childPrepend newInput )
                                                )
                                }
                            )
                        )
        }

Field.nameAlter helpers will help remove some verbosity:

import Field

... path newInput =
    Field.projectsAlter
        (Scroll.focusMap
            (Fillable.fillMap
                (Field.callsAlter
                    (List.map
                        (Tree.elementAlter
                            ( path, Tree.childPrepend newInput )
                        )
                    )
                )
            )
        )

with

module Field exposing (callsAlter, projectsAlter)

callsAlter : (calls -> calls) -> { record | calls : calls } -> { record | calls : calls }
callsAlter alter =
    \record -> { record | calls = record.calls |> alter }
...

We can reduce the number of helpers by combining the possible operations (access, replace, alter, name, ...) into a "lens":

import Field
import Hand.On
import Accessors exposing (over)
import Accessors.Library exposing (onEach)

... path newInput =
    over Field.projects --← a "lens" for the field .projects
        (over Scroll.focus
            (over Hand.On.filled --← a "lens" for the variant `Hand.Filled`
                (over Field.calls --← a "lens" for the field .calls
                    (over onEach
                        (over (Tree.elementAt path)
                            (Tree.childPrepend newInput)
                        )
                    )
                )
            )
        )

Seeing a pattern? You can, to put the cherry on the cake, compose those "lenses":

import Field
import Emptiable.On
import Accessors exposing (over)
import Accessors.Library exposing (onEach)

... path newInput =
    over                           --                  <target>
        (Field.projects            -- { _ | projects : <Scroll ...> }
            << Scroll.focus
            << Emptiable.On.filled -- type Emptiable fill = Filled <fill> | ...
            << Field.calls         -- { _ | projects : <List ...> }
            << onEach              -- List (<Tree ...>)
            << Tree.elementAt path
        )
        (Tree.childPrepend newInput)

Methods like this make your code more readable. Compare with the first example.

RecordFieldHelper.GenerateUsed automatically generates record field lenses you use.

In the last examples

RecordFieldHelper.GenerateUsed

try without installing

elm-review --template lue-bird/elm-review-missing-record-field-lens/example/field-accessors

configure

module ReviewConfig exposing (config)

import RecordFieldHelper.GenerateUsed
import Review.Rule exposing (Rule)

config : List Rule
config =
    [ RecordFieldHelper.GenerateUsed.rule
        { generator = RecordFieldHelper.GenerateUsed.accessors
        , generateIn = ( "Field", [] )
        }
    ]

See Config

lenses that work out of the box

It's also possible to generate custom helpers or to customize the generation of existing ones.

VariantHelper.GenerateUsed

Helpers for the values of one variant.

With the Config below, calling YourVariantType.onOneOfThree, the rule will automatically

  • import YourVariantType.On
  • generate non-existent prisms/lenses YourVariantType.On.variantName

try without installing

elm-review --template lue-bird/elm-review-missing-record-field-lens/example/variant-accessors

configure

module ReviewConfig exposing (config)

import Review.Rule as Rule exposing (Rule)
import VariantHelper.GenerateUsed

config : List Rule
config =
    [ VariantHelper.GenerateUsed.rule
        { build =
            VariantHelper.GenerateUsed.accessors
                { valuesCombined = VariantHelper.GenerateUsed.valuesRecord }
        , nameInModuleInternal = VariantHelper.GenerateUsed.variantAfter "on"
        , nameInModuleExternal = VariantHelper.GenerateUsed.variant
        , generationModuleIsVariantModuleDotSuffix = "On"
        }
    ]

Check out Config!

out of the box

It's also possible to generate custom helpers or to customize the generation of existing ones.

pitfalls

Don't let this pattern warp you into overusing nesting.

Structuring a model like

{ player : { position : ..., speed : ... }
, scene : { trees : ..., rocks : ... }
}

makes it unnecessarily hard to update inner fields.

organizing in blocks

type alias Model = 
    { column : Column
    , textPage : TextPage
    }

often doesn't make sense in practice where small pieces interact with one another: from "Make Data Structures" by Richard Feldman – blocks → multiple sources of truth

{ playerPosition : ...
, playerSpeed : ...
, sceneTrees : ...
, sceneRocks : ...
}

Doesn't ↑ make ui harder? Yes, but the extra explicitness is worth it. player could have things that are irrelevant to the ui like configuredControls etc. It's best to keep state structure and ui requirements separate.

Similarly, leaning towards a more limited, domain tailored API of types, packages, ... with strong boundaries will lead to easier code with stronger guarantees. ↑ example from "Make Data Structures" by Richard Feldman: Doc.id should be read-only

Don't try to design your API around lenses etc. Only if the API interaction happens to mirror that behavior, Dōzo

when is nesting acceptable?

When parts are logically connected like an Address or a Camera. Make sure to make types, packages, ... out of these. Don't obsessively employ primitives.

suggestions?

contributing