Search code examples
typeclasspurescriptrow-polymorphism

Purescript Union of Rows


I've been trying to develop a component system in Purescript, using a Component typeclass which specifies an eval function. The eval function for can be recursively called by a component for each sub-component of the component, in essence fetching the input's values.

As components may wish to use run-time values, a record is also passed into eval. My goal is for the rows in the Record argument of the top-level eval to be required to include all the rows of every sub-component. This is not too difficult for components which do not use any rows themselves, but their single sub-component does, as we can simply pass along the sub-components rows to the component's. This is shown in evalIncrement.

import Prelude ((+), one)
import Data.Symbol (class IsSymbol, SProxy(..))
import Record (get)
import Prim.Row (class Cons, class Union)

class Component a b c | a -> b where
  eval :: a -> Record c -> b

data Const a = Const a

instance evalConst :: Component (Const a) a r where
  eval (Const v) r = v

data Var (a::Symbol) (b::Type) = Var

instance evalVar :: 
  ( IsSymbol a
  , Cons a b r' r) => Component (Var a b) b r  where
  eval _ r = get (SProxy :: SProxy a) r

data Inc a = Inc a

instance evalInc :: 
  ( Component a Int r
  ) => Component (Inc a) Int r where
  eval (Inc a) r = (eval a r) + one

All of the above code works correctly. However, once I try to introduce a component which takes multiple input components and merges their rows, I cannot seem to get it to work. For example, when trying to use the class Union from Prim.Row:

data Add a b = Add a b

instance evalAdd :: 
  ( Component a Int r1
  , Component b Int r2
  , Union r1 r2 r3
  ) => Component (Add a b) Int r3 where
  eval (Add a b) r = (eval a r) + (eval b r)

The following error is produced:

  No type class instance was found for

    Processor.Component a3 
                        Int 
                        r35


while applying a function eval
  of type Component t0 t1 t2 => t0 -> { | t2 } -> t1
  to argument a
while inferring the type of eval a
in value declaration evalAdd

where a3 is a rigid type variable
      r35 is a rigid type variable
      t0 is an unknown type
      t1 is an unknown type
      t2 is an unknown type

In fact, even modifying the evalInc instance to use a dummy Union with an empty row produces a similar error, like so:

instance evalInc :: (Component a Int r, Union r () r1) 
                       => Component (Increment a) Int r1 where

Am I using Union incorrectly? Or do I need further functional dependencies for my class - I do not understand them very well.

I am using purs version 0.12.0


Solution

  • As the instance for Var is already polymorphic (or technically open?) due to the use of Row.Cons, ie

    eval (Var :: Var "a" Int) :: forall r. { "a" :: Int | r } -> Int
    

    Then all we have to is use the same record for the left and right evaluation, and the type system can infer the combination of the two without requiring a union:

    instance evalAdd :: 
      ( Component a Int r
      , Component b Int r
      ) => Component (Add a b) Int r where
      eval (Add a b) r = (eval a r) + (eval b r)
    

    This is more obvious when not using typeclasses:

    > f r = r.foo :: Int
    > g r = r.bar :: Int
    > :t f
    forall r. { foo :: Int | r } -> Int
    > :t g
    forall r. { bar :: Int | r } -> Int
    > fg r = (f r) + (g r)
    > :t fg
    forall r. { foo :: Int, bar :: Int | r } -> Int
    

    I think the downside to this approach compared to @erisco's is that the open row must be in the definition of instances like Var, rather than in the definition of eval? It is also not enforced, so if a Component doesn't use open rows then a combinator such as Add no longer works.

    The benefit is the lack of the requirement for the RProxies, unless they are not actually needed for eriscos implementation, I haven't checked.

    Update:

    I worked out a way of requiring eval instances to be closed, but it makes it quite ugly, making use of pick from purescript-record-extra.

    I'm not really sure why this would be better over the above option, feels like I'm just re-implementing row polymorphism

    import Record.Extra (pick, class Keys)
    
    ...
    
    instance evalVar :: 
      ( IsSymbol a
      , Row.Cons a b () r
      ) => Component (Var a b) b r where
      eval _ r = R.get (SProxy :: SProxy a) r
    
    data Add a b = Add a b
    
    evalp :: forall c b r r_sub r_sub_rl trash
       . Component c b r_sub
      => Row.Union r_sub trash r
      => RL.RowToList r_sub r_sub_rl
      => Keys r_sub_rl
      => c -> Record r -> b
    evalp c r = eval c (pick r)
    
    instance evalAdd :: 
      ( Component a Int r_a
      , Component b Int r_b
      , Row.Union r_a r_b r
      , Row.Nub r r_nub
      , Row.Union r_a trash_a r_nub
      , Row.Union r_b trash_b r_nub
      , RL.RowToList r_a r_a_rl
      , RL.RowToList r_b r_b_rl
      , Keys r_a_rl
      , Keys r_b_rl
      ) => Component (Add a b) Int r_nub where
      eval (Add a b) r = (evalp a r) + (evalp b r)
    
    eval (Add (Var :: Var "a" Int) (Var :: Var "b" Int) ) :: { a :: Int , b :: Int } -> Int  
    eval (Add (Var :: Var "a" Int) (Var :: Var "a" Int) ) :: { a :: Int } -> Int