Similar record types in a list/array in purescript

て烟熏妆下的殇ゞ 提交于 2021-01-27 11:44:57

问题


Is there any way to do something like

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

such that both is inferred as {x::Int | r} or something like that?

I've tried a few things:

[{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

Everything I can think of seems to tell me that combining records like this into a collection is not supported. Kind of like, a function can be polymorphic but a list cannot. Is that the correct interpretation? It reminds me a bit of the F# "value restriction" problem, though I thought that was just because of CLR restrictions whereas JS should not have that issue. But maybe it's unrelated.

Is there any way to declare the list/array to support this?


回答1:


What you're looking for is "existential types", and PureScript just doesn't support those at the syntax level the way Haskell does. But you can roll your own :-)

One way to go is "data abstraction" - i.e. encode the data in terms of operations you'll want to perform on it. For example, let's say you'll want to get the value of x out of them at some point. In that case, make an array of these:

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

If you have multiple such operations, you can always make a record of them:

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
    }

(note the Unit arguments in every function - they're there for the laziness, assuming each operation could be expensive)

But if you really need the type machinery, you can do what I call "poor man's existential type". If you look closely, existential types are nothing more than "deferred" type checks - deferred to the point where you'll need to see the type. And what's a mechanism to defer something in an ML language? That's right - a function! :-)

 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

The way this works is that RecordRep wraps a function, which takes another function, which is polymorphic in r - that is, if you're looking at a RecordRep, you must be prepared to give it a function that can work with any r. toRecordRep wraps the record in such a way that its precise type is not visible on the outside, but it will be used to instantiate the generic function, which you will eventually provide. In my example such function is _.x.

Note, however, that herein lies the problem: the row r is literally not known when you get to work with an element of the array, so you can't do anything with it. Like, at all. All you can do is get the x field, because its existence is hardcoded in the signatures, but besides the x - you just don't know. And that's by design: if you want to put anything into the array, you must be prepared to get anything out of it.

Now, if you do want to do something with the values after all, you'll have to explain that by constraining r, for example:

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

Passing the show function like this works, because we have constrained the row r in such a way that Show {x::Int|r} must exist, and therefore, applying show to {x::Int|r} must work. Repeat for your own type classes as needed.

And here's the interesting part: since type classes are implemented as dictionaries of functions, the two options described above are actually equivalent - in both cases you end up passing around a dictionary of functions, only in the first case it's explicit, but in the second case the compiler does it for you.

Incidentally, this is how Haskell language support for this works as well.




回答2:


Folloing @FyodorSoikin answer based on "existential types" and what we can find in purescript-exists we can provide yet another solution. Finally we will be able to build an Array of records which will be "isomorphic" to:

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

Let's start with type constructor which can be used to existentially quantify over a row type (type of kind #Type). We are not able to use Exists from purescript-exists here because PureScript has no kind polymorphism and original Exists is parameterized over Type.

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

We can follow and reimplement (<Ctrl-c><Ctrl-v> ;-)) definitions from Data.Exists and build a set of tools to work with such Exists values:

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

Using them we get the ability to build an Array of Records with "any" tail but we have to wrap any such a record type in a newtype before:

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

Now we can build an Array using mkExists:

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

and process values using runExists:

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


来源:https://stackoverflow.com/questions/53270182/similar-record-types-in-a-list-array-in-purescript

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!