纯脚本中 list/array 中的相似记录类型

Similar record types in a list/array in purescript

有什么办法可以做到

first = {x:0}
second = {x:1,y:1}
both = [first, second]

使得 both 被推断为 {x::Int | r} 或类似的东西?

我尝试了一些方法:

[{x:3}] :: Array(forall r. {x::Int|r})    -- nope

test = Nil :: List(forall r. {x::Int|r})
{x:1} : test                              -- nope

type X r = {x::Int | r}
test = Nil :: List(X)              -- nope
test = Nil :: List(X())
{x:1} : test
{x:1, y:1} : test                  -- nope

我能想到的一切似乎都在告诉我,不支持将这样的记录合并到一个集合中。有点像,函数可以是多态的,但列表不能。这是正确的解释吗?它让我想起了一些 F# "value restriction" 问题,虽然我认为那只是因为 CLR 限制,而 JS 不应该有那个问题。但也许它是无关的。

有什么方法可以声明 list/array 来支持这个吗?

您正在寻找的是“existential types”,而 PureScript 只是不像 Haskell 那样在语法级别上支持它们。但是你可以自己动手:-)

一种方法 是"data abstraction" - 即根据您要对其执行的操作对数据进行编码。例如,假设您希望在某个时候从它们中获取 x 的值。在这种情况下,制作一个数组:

type RecordRep = Unit -> Int

toRecordRep :: forall r. { x :: Int | r } -> RecordRep
toRecordRep {x} _ = x

-- Construct the array using `toRecordRep`
test :: Array RecordRep
test = [ toRecordRep {x:1}, toRecordRep {x:1, y:1} ]

-- Later use the operation
allTheXs :: Array Int
allTheXs = test <#> \r -> r unit

如果您有多个这样的操作,您可以随时记录它们:

type RecordRep = 
    { getX :: Unit -> Int
    , show :: Unit -> String
    , toJavaScript :: Unit -> Foreign.Object
    }

toRecordRep r = 
    { getX: const r.x
    , show: const $ show r.x
    , toJavaScript: const $ unsafeCoerce r
    }

(注意每个函数中的 Unit 参数 - 它们是为了懒惰而存在的,假设每个操作都可能很昂贵)

但是如果你真的需要打字机,你可以按我说的做"poor man's existential type"。如果你仔细观察,存在类型只不过是 "deferred" 类型检查 - 推迟到你需要查看类型的地步。在 ML 语言中推迟某些事情的机制是什么?没错——一个函数! :-)

 newtype RecordRep = RecordRep (forall a. (forall r. {x::Int|r} -> a) -> a)

 toRecordRep :: forall r. {x::Int|r} -> RecordRep
 toRecordRep r = RecordRep \f -> f r

 test :: Array RecordRep
 test = [toRecordRep {x:1}, toRecordRep {x:1, y:1}]

 allTheXs = test <#> \(RecordRep r) -> r _.x

它的工作方式是 RecordRep 包装一个函数,该函数接受另一个函数,该函数在 r 中是多态的 - 也就是说,如果您正在查看 RecordRep , 你必须准备好给它一个可以与任何 r 一起工作的函数。 toRecordRep 以这样一种方式包装记录,使其精确类型在外部不可见,但它将用于实例化您最终将提供的通用函数。在我的示例中,这样的函数是 _.x.

但是请注意,这里存在问题:当您开始使用数组的元素时,行 r 实际上是未知的,因此您无法对其执行任何操作。就像,完全一样。您所能做的就是获取 x 字段,因为它的存在是硬编码在签名中的,但除了 x - 您只是不知道。这是设计使然:如果您想将 anything 放入数组,您必须准备好从数组中获取 anything

现在,如果您确实想对这些值做些什么,您必须通过限制 r 来解释,例如:

newtype RecordRep = RecordRep (forall a. (forall r. Show {x::Int|r} => {x::Int|r} -> a) -> a)

toRecordRep :: forall r. Show {x::Int|r} => {x::Int|r} -> RecordRep
toRecordRep r = RecordRep \f -> f r

test :: Array RecordRep
test = [toRecordRep {x:1}, toRecordRep {x:1, y:1}]

showAll = test <#> \(RecordRep r) -> r show

像这样传递 show 函数是可行的,因为我们以 Show {x::Int|r} 必须存在的方式限制了行 r,因此,应用 show{x::Int|r} 必须工作。根据需要重复您自己的类型 类。

这里是有趣的部分:由于类型 类 是作为函数字典实现的,所以上面描述的两个选项实际上是等价的 - 在这两种情况下你最终传递函数字典,仅在第一种情况下它是显式的,但在第二种情况下编译器会为您完成。

顺便说一下,Haskell 语言支持也是如此。

遵循基于 "existential types" 的@FyodorSoikin 回答以及我们在 purescript-exists 中可以找到的内容,我们可以提供另一种解决方案。 最后,我们将能够构建一个 Array 的记录,这些记录将是 "isomorphic" 到:

exists tail. Array { x :: Int | tail }

让我们从类型构造函数开始,它可用于对行类型(类型 #Type)进行存在量化。我们无法在此处使用 purescript-exists 中的 Exists,因为 PureScript 没有种类多态性,并且原始 ExistsType.

上进行了参数化
newtype Exists f = Exists (forall a. f (a :: #Type))

我们可以遵循并重新实现 Data.Exists 中的 (<Ctrl-c><Ctrl-v> ;-)) 定义,并构建一组工具来处理这些 Exists 值:

module Main where

import Prelude

import Unsafe.Coerce (unsafeCoerce)
import Data.Newtype (class Newtype, unwrap)

newtype Exists f = Exists (forall a. f (a :: #Type)) 

mkExists :: forall f a. f a -> Exists f
mkExists r = Exists (unsafeCoerce r :: forall a. f a)

runExists :: forall b f. (forall a. f a -> b) -> Exists f -> b
runExists g (Exists f) = g f

使用它们,我们能够构建 RecordsArray 和 "any" 尾部,但我们必须在 newtype 之前将任何此类记录类型包装起来:

newtype R t = R { x :: Int | t }
derive instance newtypeRec :: Newtype (R t) _

现在我们可以使用 mkExists 构建一个 Array:

arr :: Array (Exists R)
arr = [ mkExists (R { x: 8, y : "test"}), mkExists (R { x: 9, z: 10}) ]

和过程值使用 runExists:

x :: Array [ Int ]
x = map (runExists (unwrap >>> _.x)) arr