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

NOTE: the latest version is 2.1.0

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.Extra.Local as Hand
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.onFilled --← a "lens" for the variant `Hand.Filled`
                (over Fields.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 Hand.On
import Accessors exposing (over)
import Accessors.Library exposing (onEach)

... path newInput =
    over                     --                  <target>
        (Field.projects      -- { _ | projects : <Scroll ...> }
            << Scroll.focus
            << Hand.On.filled -- type Hand 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.

→ NoMissingRecordFieldLens automatically generates record field lenses you use.

In the last examples

  • projects, calls lenses will be generated in module Field
  • Hand.On.filled lens will be generated in module Hand.On by VariantLens.GenerateUsed

NoMissingRecordFieldLens

try without installing

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

configure

module ReviewConfig exposing (config)

import NoMissingRecordFieldLens
import Review.Rule exposing (Rule)

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

See Config

lenses that work out of the box

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

VariantLens.GenerateUsed

Lenses for the values of one variant.

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

  • import YourVariantType.On
  • generate non-existent prisms 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 VariantLens.GenerateUsed

config : List Rule
config =
    [ VariantLens.GenerateUsed.rule
        { build =
            VariantLens.GenerateUsed.accessors
                { valuesRepresent = VariantLens.GenerateUsed.valuesRecord }
        , name = VariantLens.GenerateUsed.prismNameVariant
        , generationModuleIsVariantModuleDotSuffix = "On"
        }
    ]

Check out Config!

lenses that work out of the box

It's also possible to generate custom prisms 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. 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