行的纯脚本联合
Purescript Union of Rows
我一直在尝试使用指定 eval 函数的组件类型class 在 Purescript 中开发组件系统。对于组件的每个子组件,组件可以递归调用 eval 函数,本质上是获取输入的值。
由于组件可能希望使用 运行 时间值,因此还会将记录传递给 eval。我的目标是要求顶级 eval 的 Record 参数中的行包含每个子组件的所有行。这对于本身不使用任何行的组件来说并不太难,但它们的单个子组件确实如此,因为我们可以简单地将子组件行传递给组件的行。这显示在 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
以上所有代码都可以正常工作。但是,一旦我尝试引入一个采用多个输入组件并合并它们的行的组件,我似乎无法让它工作。例如,当尝试使用 Prim.Row
中的 class Union
时:
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)
产生以下错误:
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
事实上,即使修改 evalInc
实例以使用空行的虚拟 Union 也会产生类似的错误,如下所示:
instance evalInc :: (Component a Int r, Union r () r1)
=> Component (Increment a) Int r1 where
我是否错误地使用了 Union?还是我的 class 需要进一步的功能依赖 - 我不太了解它们。
我使用的是 purs 版本 0.12.0
r ∷ r3
但在需要 r1
和 r2
的地方使用它,因此存在类型不匹配。在需要 {a ∷ A}
或 {b ∷ B}
或 {}
的地方不能给出记录 {a ∷ A, b ∷ B}
。但是,可以这样说:
f ∷ ∀ s r. Row.Cons "a" A s r ⇒ Record r → A
f {a} = a
换句话说,f
是一个函数,在任何包含标签 "a"
且类型为 A
的记录上都是多态的。同样,您可以将 eval 更改为:
eval ∷ ∀ s r. Row.Union c s r ⇒ a → Record r → b
换句话说,eval
在至少包含 c
字段的任何记录上都是多态的。这引入了类型歧义,您必须使用代理来解决它。
eval ∷ ∀ proxy s r. Row.Union c s r ⇒ proxy c → a → Record r → b
Add 的 eval 实例变为:
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
从这里开始,r1
和 r2
变得模棱两可,因为它们不是由 r3
单独确定的。对于给定的约束,s1
和 s2
也必须是已知的。可能您可以添加一个功能依赖项。我不确定什么是合适的,因为我不确定您正在设计的程序的目标是什么。
由于使用 Row.Cons,Var 的实例已经是多态的(或者技术上是开放的?),即
eval (Var :: Var "a" Int) :: forall r. { "a" :: Int | r } -> Int
那么我们所要做的就是对左右评估使用相同的记录,并且类型系统可以推断出两者的组合而不需要联合:
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)
这在不使用类型类时更加明显:
> 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
我认为与@erisco 相比,这种方法的缺点是打开的行必须在 Var 等实例的定义中,而不是在 eval 的定义中?它也不是强制执行的,因此如果组件不使用开放行,则 Add 等组合器将不再有效。
好处是没有对 RProxies 的要求,除非 eriscos 实施实际上不需要它们,我没有检查过。
更新:
我找到了一种要求关闭 eval 实例的方法,但它使用 pick from purescript-record-extra.
使它变得非常丑陋
我不太确定为什么这会比上述选项更好,感觉就像我只是重新实现行多态性
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
我一直在尝试使用指定 eval 函数的组件类型class 在 Purescript 中开发组件系统。对于组件的每个子组件,组件可以递归调用 eval 函数,本质上是获取输入的值。
由于组件可能希望使用 运行 时间值,因此还会将记录传递给 eval。我的目标是要求顶级 eval 的 Record 参数中的行包含每个子组件的所有行。这对于本身不使用任何行的组件来说并不太难,但它们的单个子组件确实如此,因为我们可以简单地将子组件行传递给组件的行。这显示在 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
以上所有代码都可以正常工作。但是,一旦我尝试引入一个采用多个输入组件并合并它们的行的组件,我似乎无法让它工作。例如,当尝试使用 Prim.Row
中的 class Union
时:
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)
产生以下错误:
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
事实上,即使修改 evalInc
实例以使用空行的虚拟 Union 也会产生类似的错误,如下所示:
instance evalInc :: (Component a Int r, Union r () r1)
=> Component (Increment a) Int r1 where
我是否错误地使用了 Union?还是我的 class 需要进一步的功能依赖 - 我不太了解它们。
我使用的是 purs 版本 0.12.0
r ∷ r3
但在需要 r1
和 r2
的地方使用它,因此存在类型不匹配。在需要 {a ∷ A}
或 {b ∷ B}
或 {}
的地方不能给出记录 {a ∷ A, b ∷ B}
。但是,可以这样说:
f ∷ ∀ s r. Row.Cons "a" A s r ⇒ Record r → A
f {a} = a
换句话说,f
是一个函数,在任何包含标签 "a"
且类型为 A
的记录上都是多态的。同样,您可以将 eval 更改为:
eval ∷ ∀ s r. Row.Union c s r ⇒ a → Record r → b
换句话说,eval
在至少包含 c
字段的任何记录上都是多态的。这引入了类型歧义,您必须使用代理来解决它。
eval ∷ ∀ proxy s r. Row.Union c s r ⇒ proxy c → a → Record r → b
Add 的 eval 实例变为:
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
从这里开始,r1
和 r2
变得模棱两可,因为它们不是由 r3
单独确定的。对于给定的约束,s1
和 s2
也必须是已知的。可能您可以添加一个功能依赖项。我不确定什么是合适的,因为我不确定您正在设计的程序的目标是什么。
由于使用 Row.Cons,Var 的实例已经是多态的(或者技术上是开放的?),即
eval (Var :: Var "a" Int) :: forall r. { "a" :: Int | r } -> Int
那么我们所要做的就是对左右评估使用相同的记录,并且类型系统可以推断出两者的组合而不需要联合:
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)
这在不使用类型类时更加明显:
> 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
我认为与@erisco 相比,这种方法的缺点是打开的行必须在 Var 等实例的定义中,而不是在 eval 的定义中?它也不是强制执行的,因此如果组件不使用开放行,则 Add 等组合器将不再有效。
好处是没有对 RProxies 的要求,除非 eriscos 实施实际上不需要它们,我没有检查过。
更新:
我找到了一种要求关闭 eval 实例的方法,但它使用 pick from purescript-record-extra.
使它变得非常丑陋我不太确定为什么这会比上述选项更好,感觉就像我只是重新实现行多态性
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