Reputation: 2785
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
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
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