Using Rows and Variants to reduce code duplication

I’d like to learn how to reduce code duplication in my Halogen form.

I suspect there’s a way to just reconfigure the input fields in StateRows and not need to touch any other part of the code. The current version requires updating four places for each new input field (see all repeats of foo).

@kritzcreek, @cvlad, and @natefaubion shared some tips about using variants and links to these examples on slack a few months ago:

But I haven’t been able to figure out how to successfully apply these techniques to my code.

Here’s the core snippet along with instructions on how to checkout and build the example project:

git clone https://github.com/milesfrain/many_inputs.git
cd many_inputs
spago build
parcel src/index.html --open
type StateRows =
  ( foo :: String
  , bar :: String
  , baz :: String
  )

type State = Record StateRows

data Action = UpdateState State

handleAction ∷ forall o m. Action -> H.HalogenM State Action () o m Unit
handleAction = case _ of
  UpdateState s -> H.put s

mkInput :: forall r l a. IsSymbol l => Cons l String r StateRows =>
  SProxy l -> State -> HH.HTML a Action
mkInput sym st =
  HH.input
  [ HP.type_ HP.InputNumber
  , HP.value $ Record.get sym st
  , HE.onValueChange \v -> Just $ UpdateState $ Record.set sym v st
  ]

render :: forall m. State -> H.ComponentHTML Action () m
render state =
  HH.div_
    [ HH.div_ [ mkInput (SProxy::_"foo") state ]
    , HH.div_ [ mkInput (SProxy::_"bar") state ]
    , HH.div_ [ mkInput (SProxy::_"baz") state ]
    -- Print back to verify state
    , HH.div_ [ HH.text $ foldMap (\s -> s <> " ") [ state.foo, state.bar, state.baz ] ]
    ]

initialState :: forall i. i -> State
initialState _ =
  { foo: ""
  , bar: ""
  , baz: ""
  }

I also recently replaced H.modify_ with H.put for additional simplification. Is this bad practice? I can’t think of a situation where passing the new state in an action is worse than passing just the state update instructions.

If you want to only update the StateRows type to change your form, then you need a few things:

  • Initialize each field. You would need some typeclass to initialize each field and build the initial form state.
  • Render each field in a specific order. Since rows are unordered, you couldn’t use a row for StateRows unless you are happy with forms always being in lexicographical order based on the label.
  • Update each piece of state, which in simple cases can be done as part of rendering and just yielding a new state, as you’ve found out.

I don’t have specific recommendations (it seems like from other threads you are already looking into heterogeneous), but it’s possible. The type-level code will be pretty gnarly though.

A while back I put together a strawman example API for forms that you might be able to draw inspiration from, at least for the ordering part. https://gist.github.com/natefaubion/ab4d4fc31f3dee721fefea18578488fc

1 Like