Joseph Young
Joseph Young

Reputation: 2785

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

Upvotes: 6

Views: 929

Answers (2)

Joseph Young
Joseph Young

Reputation: 2785

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 

Upvotes: 1

erisco
erisco

Reputation: 14329

r ∷ r3 but it is being used where an r1 and r2 are required, so there is a type mismatch. A record {a ∷ A, b ∷ B} cannot be given where {a ∷ A} or {b ∷ B} or {} is expected. However, one can say this:

f ∷ ∀ s r. Row.Cons "a" A s r ⇒ Record r → A
f {a} = a

In words, f is a function polymorphic on any record containing a label "a" with type A. Similarly, you could change eval to:

eval ∷ ∀ s r. Row.Union c s r ⇒ a → Record r → b

In words, eval is polymorphic on any record which contains at least the fields of c. This introduces a type ambiguity which you will have to resolve with a proxy.

eval ∷ ∀ proxy s r. Row.Union c s r ⇒ proxy c → a → Record r → b

The eval instance of Add becomes:

instance evalAdd ∷
  ( Component a Int r1
  , Component b Int r2
  , Union r1 s1 r3
  , Union r2 s2 r3
  ) => Component (Add a b) Int r3 where
  eval _ (Add a b) r = eval (RProxy ∷ RProxy r1) a r + eval (RProxy ∷ RProxy r2) b r

From here, r1 and r2 become ambiguous because they're not determined from r3 alone. With the given constraints, s1 and s2 would also have to be known. Possibly there is a functional dependency you could add. I am not sure what is appropriate because I am not sure what the objectives are of the program you are designing.

Upvotes: 4

Related Questions