Purescript行的联合

6
我一直在尝试使用Purescript开发组件系统,使用一个Component类型类来指定eval函数。每个子组件都可以通过调用组件的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?或者我的类需要更多的函数依赖关系——我不太理解它们。

我正在使用 purs 版本 0.12.0。


evalAdd.eval 的定义中,你有 r :: Record r3,但调用 eval a r 需要 r :: Record r1,类似地,调用 eval b r 需要 r :: Record r2。如果它是一个普通函数,你会得到类型不匹配的错误,但由于 eval 是重载的,编译器只会说没有重载能够匹配 a + r3b + r3 - Fyodor Soikin
@FyodorSoikin 我是否需要为 evalAdd.eval 中的每个组件使用 eval 构造仅包含所需行的新记录?我不能利用开放行,以便只要 r1 是 r3 的子集,就可以传递 r3 吗? - Joseph Young
1
@JosephYoung 真的有必要使用行来存储您的值吗? 您可以使用一个Map,其键是表示Symbols(类似于reflectSymbol)的String,其值具有类型ValueValue 可以为您要存储的每种类型具有不同的构造函数。 我知道您将失去多态性,但您将在类型层面上节省费力... 或者,您可以查看 purescript-variant 包是否满足您的需求。 - Kartik Sabharwal
1
@KartikSabharwal 我已经用原始方法解决了我的问题,但还没有时间形成一个合适的答案。我有一段时间没有在这个项目上工作了,但从我记得的来看,我没有使用联合来处理两个输入的组件,而是仅仅使用同一个变量。所以本质上是用 r 替换了 r1 r2 r3,并移除了联合约束。这实现了在编译时需要输入所需变量的所有组件的效果。 - Joseph Young
@JosephYoung 谢谢你分享你的解决方案!如果您最终能够将其发布为答案,那将是非常好的,因为这种情况是使用类型类进行漂亮练习的好机会。 - Kartik Sabharwal
2个回答

4

r ∷ r3 但它被用在需要 r1r2 的地方,因此存在类型不匹配的问题。一个记录 {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

eval的Add实例变成了:
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 (RProxyRProxy r1) a r + eval (RProxyRProxy r2) b r

从这里开始,r1r2变得模糊不清,因为它们不仅由r3决定。在给定的约束条件下,还需要知道s1s2。也许您可以添加一个函数依赖关系。我不确定什么是合适的,因为我不知道您设计的程序的目标是什么。

感谢您回答我的问题(以及我看到的其他purescript问题)。 我已经发布了我认为我最终使用的方法(我从未完成该项目)。 如果我说的话有些奇怪,请随意进行评论,我已经有一段时间没有使用purescript了。 - Joseph Young

1

由于使用了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 来自 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 

也许我的代码只能工作是因为 R.get 函数的作用,它似乎表现得很奇怪,可能是由于 unsafeGet 的原因。 - Joseph Young
使用实例Component VarA Int (a :: Int | r)编写一个带有VarA的程序是可以正常工作的,其中eval _ r = r.a - Joseph Young

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接