Similar record types in a list/array in purescript
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?
purescript
add a comment |
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?
purescript
add a comment |
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?
purescript
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?
purescript
purescript
edited Nov 12 '18 at 22:56
Dax Fohl
asked Nov 12 '18 at 21:11
Dax FohlDax Fohl
7,45842773
7,45842773
add a comment |
add a comment |
2 Answers
2
active
oldest
votes
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 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.
That's interesting, you hit on exactly what I'm investigating. If classes and sets of functions are equivalent, is there a need for classes at all? i.e. instead of defining/implementing class Show, couldn't there be functions that takes objects of various types and append.show()
function to them? Of course then you'd need the ability to attach new fields to a record non-reflectively, which ps doesn't have.
– Dax Fohl
Nov 13 '18 at 18:22
Classes differ from explicit function dictionaries in that the compiler takes care of passing them around for you. Without type classes most functions would have extra parameters attached to them, which would be very awkward to use and make overloaded operators (e.g.<$>
or>>=
) impossible. Personally I'm not a big fan of that approach, but it's a thing people have explored a lot. Check out this article for example.
– Fyodor Soikin
Nov 13 '18 at 19:48
Yeah I was thinking about that one. Though I think there might be a way that combines the advantages of both, but requiring a new language. Still plotting.
– Dax Fohl
Nov 13 '18 at 20:00
add a comment |
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
The problem withpurescript-exists
is, you can't constrain the type. If the type needed to be constrained (e.g. likeShow
in my answer), the constraint would have to be baked into the definition ofExists
, which pretty much defeats the idea of it being a reusable library. And this is in addition to the other problem of it not being kind-polymorphic, which you had to work around with copy-paste.
– Fyodor Soikin
Nov 13 '18 at 23:24
I'm not sure if I understands your comment in this context @FyodorSoikin. * In original question @DaxFohl asked how to create an collection of records which share some known fields which is clearly done in my answer. We don't have to drag any constraint with this representation as we have direct access to known values. * The Lack of kind polymorphism is not a big problem becauseExists
has to be provided once per kind (so for example we now can reuse thisExists
for every type which is parametrized over row).
– paluh
Nov 13 '18 at 23:47
If the OP only cared about the value of thex
field, then he could just make an array of those values - there would be no need to make the array heterogenous, no need forExists
or anything else. The only reason to have a heterogenous array is to get access to the specific types of elements later in one way or another, andExists
does not do that.
– Fyodor Soikin
Nov 14 '18 at 0:17
I think that in general you are right but there are other possible reasons to build such an array like avoiding coping (when large parts of records are common) or debugging purposes and probably many others. Who knows what the original intent of OP was - the question was strictly about building an array ;-) RegardingExists
I'm finding it really useful for modelingGADTs
but this is another topic I think...
– paluh
Nov 14 '18 at 1:11
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "1"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53270182%2fsimilar-record-types-in-a-list-array-in-purescript%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
2 Answers
2
active
oldest
votes
2 Answers
2
active
oldest
votes
active
oldest
votes
active
oldest
votes
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 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.
That's interesting, you hit on exactly what I'm investigating. If classes and sets of functions are equivalent, is there a need for classes at all? i.e. instead of defining/implementing class Show, couldn't there be functions that takes objects of various types and append.show()
function to them? Of course then you'd need the ability to attach new fields to a record non-reflectively, which ps doesn't have.
– Dax Fohl
Nov 13 '18 at 18:22
Classes differ from explicit function dictionaries in that the compiler takes care of passing them around for you. Without type classes most functions would have extra parameters attached to them, which would be very awkward to use and make overloaded operators (e.g.<$>
or>>=
) impossible. Personally I'm not a big fan of that approach, but it's a thing people have explored a lot. Check out this article for example.
– Fyodor Soikin
Nov 13 '18 at 19:48
Yeah I was thinking about that one. Though I think there might be a way that combines the advantages of both, but requiring a new language. Still plotting.
– Dax Fohl
Nov 13 '18 at 20:00
add a comment |
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 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.
That's interesting, you hit on exactly what I'm investigating. If classes and sets of functions are equivalent, is there a need for classes at all? i.e. instead of defining/implementing class Show, couldn't there be functions that takes objects of various types and append.show()
function to them? Of course then you'd need the ability to attach new fields to a record non-reflectively, which ps doesn't have.
– Dax Fohl
Nov 13 '18 at 18:22
Classes differ from explicit function dictionaries in that the compiler takes care of passing them around for you. Without type classes most functions would have extra parameters attached to them, which would be very awkward to use and make overloaded operators (e.g.<$>
or>>=
) impossible. Personally I'm not a big fan of that approach, but it's a thing people have explored a lot. Check out this article for example.
– Fyodor Soikin
Nov 13 '18 at 19:48
Yeah I was thinking about that one. Though I think there might be a way that combines the advantages of both, but requiring a new language. Still plotting.
– Dax Fohl
Nov 13 '18 at 20:00
add a comment |
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 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.
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 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.
answered Nov 13 '18 at 16:39
Fyodor SoikinFyodor Soikin
42.2k56497
42.2k56497
That's interesting, you hit on exactly what I'm investigating. If classes and sets of functions are equivalent, is there a need for classes at all? i.e. instead of defining/implementing class Show, couldn't there be functions that takes objects of various types and append.show()
function to them? Of course then you'd need the ability to attach new fields to a record non-reflectively, which ps doesn't have.
– Dax Fohl
Nov 13 '18 at 18:22
Classes differ from explicit function dictionaries in that the compiler takes care of passing them around for you. Without type classes most functions would have extra parameters attached to them, which would be very awkward to use and make overloaded operators (e.g.<$>
or>>=
) impossible. Personally I'm not a big fan of that approach, but it's a thing people have explored a lot. Check out this article for example.
– Fyodor Soikin
Nov 13 '18 at 19:48
Yeah I was thinking about that one. Though I think there might be a way that combines the advantages of both, but requiring a new language. Still plotting.
– Dax Fohl
Nov 13 '18 at 20:00
add a comment |
That's interesting, you hit on exactly what I'm investigating. If classes and sets of functions are equivalent, is there a need for classes at all? i.e. instead of defining/implementing class Show, couldn't there be functions that takes objects of various types and append.show()
function to them? Of course then you'd need the ability to attach new fields to a record non-reflectively, which ps doesn't have.
– Dax Fohl
Nov 13 '18 at 18:22
Classes differ from explicit function dictionaries in that the compiler takes care of passing them around for you. Without type classes most functions would have extra parameters attached to them, which would be very awkward to use and make overloaded operators (e.g.<$>
or>>=
) impossible. Personally I'm not a big fan of that approach, but it's a thing people have explored a lot. Check out this article for example.
– Fyodor Soikin
Nov 13 '18 at 19:48
Yeah I was thinking about that one. Though I think there might be a way that combines the advantages of both, but requiring a new language. Still plotting.
– Dax Fohl
Nov 13 '18 at 20:00
That's interesting, you hit on exactly what I'm investigating. If classes and sets of functions are equivalent, is there a need for classes at all? i.e. instead of defining/implementing class Show, couldn't there be functions that takes objects of various types and append
.show()
function to them? Of course then you'd need the ability to attach new fields to a record non-reflectively, which ps doesn't have.– Dax Fohl
Nov 13 '18 at 18:22
That's interesting, you hit on exactly what I'm investigating. If classes and sets of functions are equivalent, is there a need for classes at all? i.e. instead of defining/implementing class Show, couldn't there be functions that takes objects of various types and append
.show()
function to them? Of course then you'd need the ability to attach new fields to a record non-reflectively, which ps doesn't have.– Dax Fohl
Nov 13 '18 at 18:22
Classes differ from explicit function dictionaries in that the compiler takes care of passing them around for you. Without type classes most functions would have extra parameters attached to them, which would be very awkward to use and make overloaded operators (e.g.
<$>
or >>=
) impossible. Personally I'm not a big fan of that approach, but it's a thing people have explored a lot. Check out this article for example.– Fyodor Soikin
Nov 13 '18 at 19:48
Classes differ from explicit function dictionaries in that the compiler takes care of passing them around for you. Without type classes most functions would have extra parameters attached to them, which would be very awkward to use and make overloaded operators (e.g.
<$>
or >>=
) impossible. Personally I'm not a big fan of that approach, but it's a thing people have explored a lot. Check out this article for example.– Fyodor Soikin
Nov 13 '18 at 19:48
Yeah I was thinking about that one. Though I think there might be a way that combines the advantages of both, but requiring a new language. Still plotting.
– Dax Fohl
Nov 13 '18 at 20:00
Yeah I was thinking about that one. Though I think there might be a way that combines the advantages of both, but requiring a new language. Still plotting.
– Dax Fohl
Nov 13 '18 at 20:00
add a comment |
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
The problem withpurescript-exists
is, you can't constrain the type. If the type needed to be constrained (e.g. likeShow
in my answer), the constraint would have to be baked into the definition ofExists
, which pretty much defeats the idea of it being a reusable library. And this is in addition to the other problem of it not being kind-polymorphic, which you had to work around with copy-paste.
– Fyodor Soikin
Nov 13 '18 at 23:24
I'm not sure if I understands your comment in this context @FyodorSoikin. * In original question @DaxFohl asked how to create an collection of records which share some known fields which is clearly done in my answer. We don't have to drag any constraint with this representation as we have direct access to known values. * The Lack of kind polymorphism is not a big problem becauseExists
has to be provided once per kind (so for example we now can reuse thisExists
for every type which is parametrized over row).
– paluh
Nov 13 '18 at 23:47
If the OP only cared about the value of thex
field, then he could just make an array of those values - there would be no need to make the array heterogenous, no need forExists
or anything else. The only reason to have a heterogenous array is to get access to the specific types of elements later in one way or another, andExists
does not do that.
– Fyodor Soikin
Nov 14 '18 at 0:17
I think that in general you are right but there are other possible reasons to build such an array like avoiding coping (when large parts of records are common) or debugging purposes and probably many others. Who knows what the original intent of OP was - the question was strictly about building an array ;-) RegardingExists
I'm finding it really useful for modelingGADTs
but this is another topic I think...
– paluh
Nov 14 '18 at 1:11
add a comment |
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
The problem withpurescript-exists
is, you can't constrain the type. If the type needed to be constrained (e.g. likeShow
in my answer), the constraint would have to be baked into the definition ofExists
, which pretty much defeats the idea of it being a reusable library. And this is in addition to the other problem of it not being kind-polymorphic, which you had to work around with copy-paste.
– Fyodor Soikin
Nov 13 '18 at 23:24
I'm not sure if I understands your comment in this context @FyodorSoikin. * In original question @DaxFohl asked how to create an collection of records which share some known fields which is clearly done in my answer. We don't have to drag any constraint with this representation as we have direct access to known values. * The Lack of kind polymorphism is not a big problem becauseExists
has to be provided once per kind (so for example we now can reuse thisExists
for every type which is parametrized over row).
– paluh
Nov 13 '18 at 23:47
If the OP only cared about the value of thex
field, then he could just make an array of those values - there would be no need to make the array heterogenous, no need forExists
or anything else. The only reason to have a heterogenous array is to get access to the specific types of elements later in one way or another, andExists
does not do that.
– Fyodor Soikin
Nov 14 '18 at 0:17
I think that in general you are right but there are other possible reasons to build such an array like avoiding coping (when large parts of records are common) or debugging purposes and probably many others. Who knows what the original intent of OP was - the question was strictly about building an array ;-) RegardingExists
I'm finding it really useful for modelingGADTs
but this is another topic I think...
– paluh
Nov 14 '18 at 1:11
add a comment |
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
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
edited Nov 13 '18 at 21:51
answered Nov 13 '18 at 21:39
paluhpaluh
1,5711511
1,5711511
The problem withpurescript-exists
is, you can't constrain the type. If the type needed to be constrained (e.g. likeShow
in my answer), the constraint would have to be baked into the definition ofExists
, which pretty much defeats the idea of it being a reusable library. And this is in addition to the other problem of it not being kind-polymorphic, which you had to work around with copy-paste.
– Fyodor Soikin
Nov 13 '18 at 23:24
I'm not sure if I understands your comment in this context @FyodorSoikin. * In original question @DaxFohl asked how to create an collection of records which share some known fields which is clearly done in my answer. We don't have to drag any constraint with this representation as we have direct access to known values. * The Lack of kind polymorphism is not a big problem becauseExists
has to be provided once per kind (so for example we now can reuse thisExists
for every type which is parametrized over row).
– paluh
Nov 13 '18 at 23:47
If the OP only cared about the value of thex
field, then he could just make an array of those values - there would be no need to make the array heterogenous, no need forExists
or anything else. The only reason to have a heterogenous array is to get access to the specific types of elements later in one way or another, andExists
does not do that.
– Fyodor Soikin
Nov 14 '18 at 0:17
I think that in general you are right but there are other possible reasons to build such an array like avoiding coping (when large parts of records are common) or debugging purposes and probably many others. Who knows what the original intent of OP was - the question was strictly about building an array ;-) RegardingExists
I'm finding it really useful for modelingGADTs
but this is another topic I think...
– paluh
Nov 14 '18 at 1:11
add a comment |
The problem withpurescript-exists
is, you can't constrain the type. If the type needed to be constrained (e.g. likeShow
in my answer), the constraint would have to be baked into the definition ofExists
, which pretty much defeats the idea of it being a reusable library. And this is in addition to the other problem of it not being kind-polymorphic, which you had to work around with copy-paste.
– Fyodor Soikin
Nov 13 '18 at 23:24
I'm not sure if I understands your comment in this context @FyodorSoikin. * In original question @DaxFohl asked how to create an collection of records which share some known fields which is clearly done in my answer. We don't have to drag any constraint with this representation as we have direct access to known values. * The Lack of kind polymorphism is not a big problem becauseExists
has to be provided once per kind (so for example we now can reuse thisExists
for every type which is parametrized over row).
– paluh
Nov 13 '18 at 23:47
If the OP only cared about the value of thex
field, then he could just make an array of those values - there would be no need to make the array heterogenous, no need forExists
or anything else. The only reason to have a heterogenous array is to get access to the specific types of elements later in one way or another, andExists
does not do that.
– Fyodor Soikin
Nov 14 '18 at 0:17
I think that in general you are right but there are other possible reasons to build such an array like avoiding coping (when large parts of records are common) or debugging purposes and probably many others. Who knows what the original intent of OP was - the question was strictly about building an array ;-) RegardingExists
I'm finding it really useful for modelingGADTs
but this is another topic I think...
– paluh
Nov 14 '18 at 1:11
The problem with
purescript-exists
is, you can't constrain the type. If the type needed to be constrained (e.g. like Show
in my answer), the constraint would have to be baked into the definition of Exists
, which pretty much defeats the idea of it being a reusable library. And this is in addition to the other problem of it not being kind-polymorphic, which you had to work around with copy-paste.– Fyodor Soikin
Nov 13 '18 at 23:24
The problem with
purescript-exists
is, you can't constrain the type. If the type needed to be constrained (e.g. like Show
in my answer), the constraint would have to be baked into the definition of Exists
, which pretty much defeats the idea of it being a reusable library. And this is in addition to the other problem of it not being kind-polymorphic, which you had to work around with copy-paste.– Fyodor Soikin
Nov 13 '18 at 23:24
I'm not sure if I understands your comment in this context @FyodorSoikin. * In original question @DaxFohl asked how to create an collection of records which share some known fields which is clearly done in my answer. We don't have to drag any constraint with this representation as we have direct access to known values. * The Lack of kind polymorphism is not a big problem because
Exists
has to be provided once per kind (so for example we now can reuse this Exists
for every type which is parametrized over row).– paluh
Nov 13 '18 at 23:47
I'm not sure if I understands your comment in this context @FyodorSoikin. * In original question @DaxFohl asked how to create an collection of records which share some known fields which is clearly done in my answer. We don't have to drag any constraint with this representation as we have direct access to known values. * The Lack of kind polymorphism is not a big problem because
Exists
has to be provided once per kind (so for example we now can reuse this Exists
for every type which is parametrized over row).– paluh
Nov 13 '18 at 23:47
If the OP only cared about the value of the
x
field, then he could just make an array of those values - there would be no need to make the array heterogenous, no need for Exists
or anything else. The only reason to have a heterogenous array is to get access to the specific types of elements later in one way or another, and Exists
does not do that.– Fyodor Soikin
Nov 14 '18 at 0:17
If the OP only cared about the value of the
x
field, then he could just make an array of those values - there would be no need to make the array heterogenous, no need for Exists
or anything else. The only reason to have a heterogenous array is to get access to the specific types of elements later in one way or another, and Exists
does not do that.– Fyodor Soikin
Nov 14 '18 at 0:17
I think that in general you are right but there are other possible reasons to build such an array like avoiding coping (when large parts of records are common) or debugging purposes and probably many others. Who knows what the original intent of OP was - the question was strictly about building an array ;-) Regarding
Exists
I'm finding it really useful for modeling GADTs
but this is another topic I think...– paluh
Nov 14 '18 at 1:11
I think that in general you are right but there are other possible reasons to build such an array like avoiding coping (when large parts of records are common) or debugging purposes and probably many others. Who knows what the original intent of OP was - the question was strictly about building an array ;-) Regarding
Exists
I'm finding it really useful for modeling GADTs
but this is another topic I think...– paluh
Nov 14 '18 at 1:11
add a comment |
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Some of your past answers have not been well-received, and you're in danger of being blocked from answering.
Please pay close attention to the following guidance:
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53270182%2fsimilar-record-types-in-a-list-array-in-purescript%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown