【问题标题】:Purescript Union of RowsPurescript 行联合
【发布时间】:2018-07-19 12:14:47
【问题描述】:

我一直在尝试在 Purescript 中开发一个组件系统,使用指定 eval 函数的 Component 类型类。 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.Rowclass 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 实例以使用具有空行的虚拟联合也会产生类似的错误,如下所示:

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 没有重载。
  • @FyodorSoikin 对于 evalAdd.eval 中的组件每次使用 eval,我是否必须构建一个仅包含组件所需行的新记录?我可以不使用开放行吗?只要 r1 是 r3 的子集,那么传递 r3 就可以了?
  • @JosephYoung 真的有必要使用一行来存储您的值吗?您可以使用Map,其键为Strings,代表Symbols(类似于reflectSymbol),其值的类型为ValueValue 可以为您要存储的每种类型使用不同的构造函数。我知道你会失去多态性,但你会节省类型级别的体操......或者你可以看看 purescript-variant 包是否满足你的需求。
  • @KartikSabharwal 我实际上已经使用原始方法解决了我的问题,但还没有时间形成正确的答案。我有一段时间没有从事这个项目了,但据我记得,我没有对两个输入的组件使用联合,而是对每个输入使用相同的变量。所以本质上用r 替换r1 r2 r3 并删除联合约束。这达到了要求组件在类型级别的编译时需要其输入所需的所有变量的效果
  • @JosephYoung 感谢分享您的解决方案!如果您最终可以将其作为答案发布,那就太好了,因为这种情况是使用类型类的巧妙练习。

标签: typeclass purescript row-polymorphism


【解决方案1】:

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

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

从这里开始,r1r2 变得模棱两可,因为它们不是由 r3 单独确定的。在给定的约束条件下,s1s2 也必须是已知的。可能存在您可以添加的功能依赖项。我不确定什么是合适的,因为我不确定您正在设计的程序的目标是什么。

【讨论】:

  • 感谢您回答我的问题(以及我看到的其他纯脚本问题)。我已经发布了我认为我最终使用的方法(我从未完成过该项目)。如果我说的有些奇怪,请随时发表评论,我已经有一段时间没有使用 purescript了
【解决方案2】:

由于使用 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
  • Component VarA Int (a :: Int | r)eval _ r = r.a 编写一个 VarA 实际上可以正常工作
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-03-14
  • 1970-01-01
  • 1970-01-01
  • 2014-12-11
  • 1970-01-01
相关资源
最近更新 更多