Purescript 将 cons 定义为 typeclass operator 长答案旧答案 - 在问题发生重大变化之前写的

问题描述

Cons operator is defined (:) 是为 Array (Array.cons) 和 List (Cons 类型构造函数) 定义的。所以要在代码中使用它,我们应该:

import Data.List ((:))

import Data.Array ((:))

我想知道是否可以定义 (:) 以便它可以导入到一个模块中并用于同一模块中的 Array 和 List。

我尝试这样做:

class Cons container element where
  cons :: element -> container -> container

instance consArray :: Cons (Array a) a where
  cons = Array.cons

instance constList :: Cons (List a) a where
  cons = List.Cons

infixr 6 cons as :

它似乎适用于基本情况:

arr :: Array Int
arr = 1 : [2,3]

lst :: List Int
lst = 1 : (2 :3 : Nil)

但是一些高级案例,例如:

data X a = X a

getX :: ∀ msg. Int -> X msg
getX = unsafeCoerce

getCons :: ∀ msg. 
  Array (X msg) ->
  Array (X msg)
getCons children = getX 1 : children

但它给出了错误

No type class instance was found for

    MyModule.Cons (Array (X msg2))
                       (X t3)

  The instance head contains unkNown type variables. Consider adding a type annotation.

while applying a function cons
  of type Cons t0 t1 => t1 -> t0 -> t0
  to argument getX 1
while inferring the type of cons (getX 1)
in value declaration getCons

where msg2 is a rigid type variable
        bound at (line 0,column 0 - line 0,column 0)
      t0 is an unkNown type
      t1 is an unkNown type

那么问题是,一般情况下是否有可能实现我所描述的?

UPD:

这个:

class Cons container where
  cons :: forall element. element -> container element -> container element

解决了构造Array或List时(:)的统一使用问题。

因此,在导入此 (:) 后,我们可以在同一模块中执行:

arrFn :: ∀ a. a -> Array a -> Array a
arrFn el array = el : array

listFn :: ∀ a. a -> List a -> List a
listFn el list = el : el : list


-- and even this will work too:

data X a = X a

getX :: ∀ msg. Int -> X msg
getX = unsafeCoerce

getCons :: ∀ msg. 
  Array (X msg) ->
  Array (X msg)
getCons children = getX 1 : children

问题是这样定义的运算符不能用于模式匹配(因为模式匹配只适用于类型构造函数),这会产生错误

matchList :: List Int -> Int
matchList ls =
   case ls of
     Nil -> 0
     (x : xs) -> x 

而且似乎不可能实现 (:) 的完全通用用法,因此它适用于 Array/List 构造和 List 模式匹配。

解决方法

TL;DR:你需要一个函数依赖。

长答案

发生这种情况是因为编译器不知道它应该查找 Cons 的哪个实例。

在表达式 getX 1 : children 中,编译器知道 children :: Array (X msg),但是 getX 1 的类型是什么?函数 getX 可以返回任何类型的 X,任何类型。那么它应该是X Int吗?还是X String?或者,也许,X Boolean?没有办法告诉!因此,编译器只是为一些未知的类型 X t3 调用该类型 t3,然后从那里继续,希望 t3 会及时为人所知。

但事实并非如此。编译器必须解决的下一件事是应用 (:) 运算符。要做到这一点,它需要找到一个实例 Cons (Array (X msg)) (X t3),但它不知道如何找到,因为它不知道 t3 是什么,并且现有实例中没有一个与该形状匹配。


顺便说一下,您可以在此时停下来验证一下。将您的实例更改为:

instance consArray :: Cons (Array a) b where
  cons _ xs = xs

在那之后,getCons 突然编译。为什么?因为当替换 Cons (Array a) bCons (Array (X msg)) (X t3) 时,新的实例头 a ~ X msg 实际上与所需的 b ~ X t3 匹配。而且它从来没有真正出现过 t3 究竟是什么,所以它可以被忽略。

顺便说一句,您甚至不需要 X 来实现这一点。您可以通过以下方式重现问题:

getX :: forall a. Int -> a
getCons :: forall msg. Array msg -> Array msg

X 只会混淆视听。


但是“通常是否可以实现我所描述的”,您会问吗?

好吧,你还没有真正描述你想要的结果,所以很难确定。但是,如果我不得不猜测,在我看来,您真正想要的是让编译器弄清楚,因为 (:) 的第二个参数是 Array (X msg),那么第一个参数必须是该数组的一个元素 - 即 X msg,- 然后使用该信息来推断此实例化中 getX 的预期类型。

如果这确实是您想要实现的目标,那么您需要的是一个functional dependency

class Cons container element | container -> element where
                             ^^^^^^^^^^^^^^^^^^^^^^
                                    |
                               this bit here

这段语法告诉编译器,如果 container 以某种方式已知,那么 element 也必须是已知的。

在实践中它有两个作用:

  1. 您不能声明任何具有相同 container 但不同 element 的实例。例如,Cons (Array Int) IntCons (Array Int) String 不起作用。编译器会抱怨他们违反了函数依赖。
  2. 但作为回报,编译器现在可以通过知道 element 来推断 container

因此,如果您只添加该位,getCons 将可以正常编译:编译器将首先理解 container ~ Array (X msg),因此它会选择匹配的实例 - Const (Array a) a,- 并且来自将推断出 element ~ X msg,因此 getX :: Int -> X msg


旧答案 - 在问题发生重大变化之前写的

这与在类型类中定义运算符无关。如果您只是从 Data.Array 导入运算符,则会发生完全相同的事情,只是错误消息会好一些。

问题在于您试图将 X a 类型的值与 Tuple String (X a) 类型的值数组相结合。类型不同。 Tuple String (X a)X a 不同。

要使其工作,您必须修复类型签名以使类型相同:

xFn :: ∀ a. Tuple String (X a) -> Array (Tuple String (X a)) -> Array (Tuple String (X a))
xFn el list = el : list

或者你必须在尝试 cons 之前用 el 构造一个元组:

xFn :: ∀ a. X a -> Array (Tuple String (X a)) -> Array (Tuple String (X a))
xFn el list = Tuple "foo" el : list

无论哪种方式,元素都必须在某个时刻转换为元组。哪种方式“正确”取决于您的具体情况。


或者,您可以提供一个实例 Cons (Array (Tuple String a)) a,并将元素转换为该实例内的元组。没有这样的实例是编译器在错误消息中抱怨的。它可能看起来像这样:

instance consTuple :: Cons (Array (Tuple String a)) a where
    cons a xs = cons (Tuple "foo" a) xs

除非这样的实例不起作用,因为它会与 consArray 重叠。所以你可以通过将它们放在一个链中来解决那个

instance consArray :: Cons (Array a) a where
  cons = Array.cons
else instance consTuple :: Cons (Array (Tuple String a)) a where
  cons a xs = cons (Tuple "foo" a) xs

虽然我无法想象您可以为此拥有什么可能的用例。