purescript列表/数组中的类似记录类型

时间:2018-11-12 21:11:59

标签: 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#的“值限制”问题,尽管我认为这仅仅是因为CLR限制,而JS却没有这个问题。但这也许无关。

有什么方法可以声明列表/数组来支持这一点吗?

2 个答案:

答案 0 :(得分:3)

您要查找的是“ existential types”,PureScript仅不支持Haskell支持的语法级别的语法。但是你可以自己滚:-)

一种可行的方法是“数据抽象”-即根据您要对其执行的操作对数据进行编码。例如,假设您希望在某个时候从其中获得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参数-假设每个操作都可能很昂贵,它们很懒惰)

但是,如果您确实需要类型机制,则可以执行我所谓的“穷人的生存类型”。如果仔细观察,存在性类型仅是“延迟”类型检查-推迟到需要查看类型的地方。延迟ML语言中的某些内容的机制是什么?是的-函数! :-)

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

 toRecordRep :: forall r. {x::Int|r} -> RecordRep
 toRecordRep r 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之外-您只是不知道。这是设计使然:如果要将任何东西放入数组中,则必须准备好从其中获取任何东西

现在,如果您确实想对值做一些事情,则必须通过约束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函数是可行的,因为我们以必须存在r的方式约束了Show {x::Int|r}行,因此将show应用于{ {1}}必须起作用。根据需要重复输入自己的类型类。

这是有趣的部分:由于类型类是作为函数的字典实现的,因此上述两个选项实际上是等效的-在两种情况下,您最终都会传递一个函数字典,仅在第一种情况是显式的,而第二种情况是编译器为您完成的。

顺便说一下,这也是Haskell语言对此的支持方式。

答案 1 :(得分:1)

根据“现有类型”以及我们在purescript-exists中所找到的答案来关注@FyodorSoikin,我们可以提供另一种解决方案。 最后,我们将能够建立Array的记录,这些记录将“同构”为:

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

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

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

使用它们,我们可以构建带有{any“尾部的Array的{​​{1}},但是我们必须在Records之前将任何这样的记录类型包装起来:

newtype

现在我们可以使用newtype R t = R { x :: Int | t } derive instance newtypeRec :: Newtype (R t) _ 构建一个Array

mkExists

并使用arr :: Array (Exists R) arr = [ mkExists (R { x: 8, y : "test"}), mkExists (R { x: 9, z: 10}) ] 处理值:

runExists