Dispatching on row fields

variant
row-typing
#1

Hi all,

I’m trying to figure out a way to dispatch functions on the field names of a record. The case_ function in Data.Variant modules is almost exactly what I am looking for, since the exhaustive match requirement can catch a lot of problems at compile. In the example in the documentation, we have to list the specific field names in the types (like the function f in the snippet below).

That works, but if I add or remove types from the record, I would have to make sure I make the corresponding change to all the type signatures for all the function that use case_. Instead, I’m trying to figure out a type constructor where you can pass in a record type and have it spit out the appropriate variant type (something like function g):

import Data.Variant (Variant, on, case_)
import Data.Symbol (SProxy(..), class IsSymbol)
import Data.Newtype (class Newtype)
import Prim.Row (class Cons)

newtype Params = Params { phase :: Int, voltage :: Number }
derive instance paramsNewtype :: Newtype Params _

f :: Variant (phase :: Unit, voltage :: Unit) -> String
f = case_
  # on (SProxy :: SProxy "phase") (const "Dispatch phase...")
  # on (SProxy :: SProxy "voltage") (const "Dispatch voltage...")

type ParamVariant p = forall sym fields tail.
     IsSymbol sym
  => Newtype p (Record fields)
  => Cons sym Unit tail fields
  => Variant fields

g :: ParamVariant Params -> String
g = case_
  # on (SProxy :: SProxy "phase") (const "Dispatch phase...")
  # on (SProxy :: SProxy "voltage") (const "Dispatch voltage...")

The compiler complains with:

  Could not match constrained type


    IsSymbol sym0 => Newtype Params (Record fields1) => Cons sym0 Unit tail2 fields1 => Variant
 fields1


  with type

    Variant t3

Can anyone give me a pointer on this? Please let me know if the question needs more elaboration.

Thanks!

#2

That works, but if I add or remove types from the record, I would have to make sure I make the corresponding change to all the type signatures for all the function that use case_ .

I couldn’t find a solution with your current Newtype approach, but it sounds like you want to be able to modify the labels in a record without having to change the type signatures everywhere in your code base when they refer to the labels in that record.

If so, why not define a type alias to the rows in your record?

type RecordRows otherRows =
 ( phase :: Unit
 , voltage :: Unit
 | otherRows
 )

-- no other rows needed, so close the rows with an
-- empty one (i.e. '()')...
type MainRecord = { | RecordRows () }

-- Refer to type alias for rows.
-- Now we can update our rows at anytime
-- and type signatures don't need updating 
-- (though our value-level code will need to be updated).
g :: Variant RecordRows -> String
g = case_
  # on (SProxy :: SProxy "phase") (const "Dispatch phase...")
  # on (SProxy :: SProxy "voltage") (const "Dispatch voltage...")
1 Like
#3

@JordanMartinez’s solution is the one I would use, but I’ll elaborate about the error.

The main issue around the type error is that type synonyms don’t work that way. If you expand the type synonym it is equivalent to:

g ::
  (forall sym fields tail
     . IsSymbol sym
    => Newtype p (Record fields)
    => Cons sym Unit tail fields
    => Variant fields)
  -> String

Note the bracketing. This is not equivalent to:

g :: forall sym fields tail
   . IsSymbol sym
  => Newtype p (Record fields)
  => Cons sym Unit tail fields
  => Variant fields
  -> String

Note the lack of bracketing. Your code is a rank-n type, and so what you are saying is that you want to take a Variant that is polymorphic over those constraints, but that’s not what case_ expects (which is some concrete Variant). Note there are other issues with this type and the set of constraints, but that’s the immediate error.