-
Notifications
You must be signed in to change notification settings - Fork 41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enforce parametricity #30
Conversation
Another option is to include explicit map :: forall f a b. (Functor f) => f a ~> (a -> b) -> f b |
Good idea, although type signatures will become a bit noisier. There is another unrelated issue with type signatures, btw. Restrictions like |
Just noticed that you've included |
Maybe I'm missing the point but |
@ivenmarquardt Yeah, you're probably right here. I've just was trying to explain parametricity better based on type signature, and say something like " So it all comes down to explaining parametricity in the spec again. |
Hm, actually I was wrong, sorry. |
I think it's confusing because we're trying to use syntax designed for Haskell in JS. In Haskell signature But in |
N.B. You can probably skip this next part, but I want to set up some context.I think there's an important difference between the function being defined and the function being implemented. I'm going to talk abstractly about it, because there are too many details to flesh out here. This syntax: class Functor f where
fmap :: (a -> b) -> f a -> f b defines a function with the actual type This syntax: instance Functor List where
fmap = undefined provides an implementation of On to my pointIn FL, we only spec out the We have to do all the dirty work that the compiler does, but we also changed the signatures around when converting. The simplest move is to make the signatures more accurate. SL could provide functions with more accurate signatures: // map :: Functor f -> (a -> b, f a) -> f b
function map(functor) {
return functor.map;
} There's your "global" Could we implement something more similar to type classes in js? Probably. I think @CrossEye has tossed around the idea a few times. Maybe somebody should attempt to do that. But we currently don't, so we end up where we're conflating both the The point, though, is that it doesn't have to be! |
Thanks @joneshf , you've put my mind in order :)
Yea, I probably shouldn't say that it's impossible. Still nobody came up with a good way of doing it in JS as far as I know. There is some discussion even in this repo #3 , and I like your idea of providing concrete type at the end, need to think more about it.
Small remark: If I understand correctly Translating this to function call it would look like |
Could you point me to what you're talking about? I'm missing context. |
@isiahmeadows This was a response to #30 (comment) Edit: ah, sorry, I've meant to mention @ivenmarquardt there. |
We must replace all occurrences of map :: forall f a b. (Functor f) => f a ~> (a -> b) -> f b
-- replace `(Functor f) => f` with `List`
List$prototype$map :: forall a b. List a ~> (a -> b) -> List b
I followed the form of the
I believe the idea is that all type variables are introduced (
Is
Full credit should go to @tel for this idea. :) |
Yea, but we can't use concrete types in the spec. Something like the following looks good to me though:
Also
Right and this is most likely correct, but I just don't see how
Yea, for fantasy-land this is easier, I meant for static-land. |
Clearly I've been thinking a lot about Fantasy Land recently. 🌈 I'm sorry for making an unhelpful comment. |
I like it:
I wonder what the advantage of |
I like how this could look (although IMHO I feel it's a bit awkward). Here's a quick summary of the existing types using that idea (I've changed
|
I almost feel that either Haskell's |
Probably not much - that usage is already implicit in Haskell IIRC. |
This looks interesting, @isiahmeadows . Could you provide some examples of how this could look like? I've opened a PR about signatures #31 , let's move this discussion there. |
a7e6cfc
to
778db30
Compare
I've updated parametricity explanation and would appreciate any feedback! |
} | ||
``` | ||
|
||
And here's one that violates parametricity requirement although fits into type signature otherwise: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be helpful to mention why violating parametricity is bad. Namely because it'll break something
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like Functor's composition law? Sounds like a good idea, but one may think "why have parametricity requirement then if we already have corresponding laws?".
As far as I understand parametricity actually gives composition law for free, and I'm guessing that it gives more than that. So the value of this requirement is to have that extra value that parametricity gives besides laws that we already have anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. But, this example sometimes breaks the identity law.
> X.map(x => x, {x: false})
{ x: false }
> X.map(x => x, {x: {}})
{ x: {} }
> X.map(x => x, {x: "wat"})
{ x: 'wat' }
> X.map(x => x, {x: {hello: "world"}})
{ x: { hello: 'world' } }
> X.map(x => x, {x: new Date()})
{ x: 2016-10-24T14:45:47.112Z }
> X.map(x => x, {x: 1})
{ x: 2 }
> X.map(x => x, {x: [1,2,3]})
{ x: [] }
Might need a more complex example to show where it breaks something more spectacularly. Like the Maybe
data type that people usually start with, which checks null and whatnot.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's hard to came up with a good example. I have the following so far, it still seems contrived, and grew too big for a spec to my taste.
Here is another example of a Maybe type that violates parametricity requirement:
Maybe.Nothing = {type: 'Nothing'}
Maybe.Just = x => {type: 'Just', value: x}
Maybe.map = (f, v) => {
if (v.type === 'Nothing') {
return v
}
const a = v.value
const b = f(a)
if (b === undefined) {
return Maybe.Nothing
}
return Maybe.Just(b)
}
We make implicit conversion of undefined
to Nothing
.
This looks convenient because we can now do safer operations with arrays for example:
const first = arr => arr[0]
const inc = n => n + 1
inc(first([])) // NaN
Maybe.map(inc, Maybe.map(first, Maybe.Just([]))) // Nothing
But what if undefined
is a ligitimate value for us:
const first = arr => arr[0]
const isUndefined = x => x === undefined
Maybe.map(isUndefined, Maybe.map(first, Maybe.Just([undefined]))) // Nothing, but we expect Just(true)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also the less contrived I'm trying to make it the bigger it grows :)
I think the purpose of example here should be not to explain why we require parametricity, but rather to explain what exactly we require. Answers to why questions are very important but should not live in a specification, imo.
So maybe current example is good enough, or we could switch to a Maybe example but leave only first code snippet, with an additional code comment:
if (b === undefined) { // this is not allowed
return Maybe.Nothing
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's a common example of a parametricity violation (and the havoc it causes)
// Assume `goodMap : (a -> b, Maybe a) -> Maybe b`
// Same type, but devious!
// badMap : (a -> b, Maybe a) -> Maybe b
function badMap(f, m) {
if (m.value == 0) {
return Maybe.Nothing
} else {
return goodMap(f, m)
}
}
This is obviously cheating—it relies on (heterogenous) equality being defined for all values in Javascript—but nothing about the bare type variable a
reflects that idea. If parametricity holds then we know certain so-called "free theorems" about the behavior of badMap
, but since we've violated parametricity those will not hold.
Here's the free laws we can expect: http://www-ps.iai.uni-bonn.de/cgi-bin/free-theorems-webui.cgi where you type in (a -> b) -> (Maybe a -> Maybe b)
and click "hide type instantiations" and then look at the bottom where "Reducing all permissable relation variables to functions" has happened
-- our function is named `f`
(forall x . h (p x) = q (g x)) ==> forall x . fmap h (f p x) = f q (fmap g y) -- or
h . p = q . g ==> fmap h . f p = f q . fmap g
or, any such function should commute with fmap
so long as the lifted functions commute as well. Let h = p = q = g
and we'll see that any such function should commute with fmap
generally.
fmap g . f g = f g . fmap g
This obvious is violated by badmap since it has special behavior which blows up on hitting a particular value and that blow-up won't commute.
I honestly think that while the concept of parametricity and higher order generalization are super wonderful, that Haskell's typeclasses as the mechanism for popularizing these ideas are harmful. Typeclasses combine (1) higher order abstraction and (2) implicit search together into an incredibly usable language facility that Haskell and basically no other language has. The important thing to steal is (1), not (2). So, I'd really recommend taking more points from OCaml/SML here. The thing that static-land calls a "type" is what OCaml calls a module. The fact that it's abstracting over some concrete details is specified using a signature which "erases" some information. You can then talk about parametricity by saying that anything that passes through that erasure process properly has to have really, truly, fully forgotten all of the information it doesn't use. Because really that's what parametricity is. It's a property of types that says, roughly, that certain values can take a type which is maximally forgetful, or, seen from the other side, really parametric types heavily constrain the values that might be represented. If I ask you for a type which has forgotten many details then (a) you're heavily restricted in what you can return, and (b) I can guess a lot of its properties. If I give you such a value then you can use it in many specific manners. |
Which is pretty much where I was coming from with this. |
@isiahmeadows @tel Sorry, I have very little knowledge of OCaml/SML, so it's hard for me to understand what you're saying. Could you explain it in vocabulary of JavaScript and this specification? Are you suggesting that we should not add parametricity requirement, or that it should be stated somehow differently, or something else? Edit: if this about type signatures, lets move it to #31 . |
@tel What do you mean with "implicit search" in terms of typeclasses? Can you provide an alternative term, so that I can google it? |
@rpominov Yeah, sorry, I should probably preface that I'm pretty far on the outside of this issue, but my name was referenced and so I happened to read the thread. I've also got a little bit of experience here in thinking through how to translate these ideas to Javascript both in some interactions I've had with the Ramda and Fantasyland folks as well as my own (experimental, abandoned) approach over at tel/js-algebraic. So, let me start with the fast answers:
So first on parametricity: it's completely required to make types make much of any sense at all. Departures from parametricity can almost exactly be equated with the degree to which types are lying. The formal definition is very powerful, but the informal definition takes most weight around the semantics of type variable binding: Second, the idea of values taking the type // type FunctorDict f = { map: forall a b . (a -> b) -> (f a -> f b) }
// ListFunctor : FunctorDict List
const ListFunctor = {
map: f => l => l.map(f)
}
// forall f . FunctorDict f -> (a -> b) -> (f a -> f b)
function map(dict) { return dict.map } This eliminates the implicit passing of the (* fake ocaml syntax to make this a little more approachable for people with haskell familiarity *)
signature FUNCTOR = sig
type t
val map : forall a b . a -> b -> (t a -> t b)
end
signature EXAMPLE = sig
type t a
val idMap : forall a . t a -> t a
end
module Example(F : FUNCTOR): EXAMPLE = struct
type t a = F.t a
val idMap = F.map identity
end Anyway, you can see how OCaml talks about "dependent modules" essentially as a function between modules. It's exactly that actually except that in order to have proper type theoretic properties we need to have the "value language" and the "module language" separate from one another. |
@tel This is still way over my head, but I think at least partially what you're saying is that language of this spec is far from perfect. For instance what we call "Type" should be called differently. And I'm totally agree with that! Also I agree that because we don't have any representation of type classes in Current state of the spec is as good as my current education in this areas goes, it will slowly improve as I learn more, but I'd gladly accept any help of course to boost that improvement 😄 |
@rpominov I'm mostly responding to some of the questions raised above with discussing parametricity, but yeah you're right on in saying that Haskell typeclasses can pose a significant challenge to translation of these ideas into Js. I'll bow out for the moment since I think the more focused question about parametricity is most important—but I think that this is always something of a struggle with coming to understand Haskell's representation of these kinds of laws and structures. Seeing them in other contexts like OCaml (or Agda) can be good for stripping away unessential details. |
## Parametricity | ||
|
||
All methods' implementations should only use type information about arguments that is known from the | ||
method's type signature. It's not allowed to inspect arguments or values that they produce to get |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Conversely, definitions should reflect every aspect of the values it manipulates in their types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, but in our case specification provides type signatures to which implementations must fit.
} | ||
``` | ||
|
||
And here's one that violates parametricity requirement although fits into type signature otherwise: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's a common example of a parametricity violation (and the havoc it causes)
// Assume `goodMap : (a -> b, Maybe a) -> Maybe b`
// Same type, but devious!
// badMap : (a -> b, Maybe a) -> Maybe b
function badMap(f, m) {
if (m.value == 0) {
return Maybe.Nothing
} else {
return goodMap(f, m)
}
}
This is obviously cheating—it relies on (heterogenous) equality being defined for all values in Javascript—but nothing about the bare type variable a
reflects that idea. If parametricity holds then we know certain so-called "free theorems" about the behavior of badMap
, but since we've violated parametricity those will not hold.
Here's the free laws we can expect: http://www-ps.iai.uni-bonn.de/cgi-bin/free-theorems-webui.cgi where you type in (a -> b) -> (Maybe a -> Maybe b)
and click "hide type instantiations" and then look at the bottom where "Reducing all permissable relation variables to functions" has happened
-- our function is named `f`
(forall x . h (p x) = q (g x)) ==> forall x . fmap h (f p x) = f q (fmap g y) -- or
h . p = q . g ==> fmap h . f p = f q . fmap g
or, any such function should commute with fmap
so long as the lifted functions commute as well. Let h = p = q = g
and we'll see that any such function should commute with fmap
generally.
fmap g . f g = f g . fmap g
This obvious is violated by badmap since it has special behavior which blows up on hitting a particular value and that blow-up won't commute.
|
||
All methods' implementations should only use type information about arguments that is known from the | ||
methods' type signatures. It's not allowed to inspect arguments or values that they produce | ||
or contain to get more information about their types. In other words methods |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like that it says "... to get more information about their types". When we do something like a === undefined
we don't really inspect to get more information about type but rather use ===
operation that works with any types, so it kinda technically legitimate.
Don't know how to say it better. Any suggestions?
@rpominov I'm not very experienced with OCaml (I've only really just toyed with it a few times), but OCaml's modules are like JavaScript's object namespaces + TypeScript's interfaces, just with a few other useful goodies (e.g. module functors = (For similar reasons, the WebAssembly reference interpreter could be hard to read for those unfamiliar with OCaml syntax.) |
Ok, this is still not perfect but I guess it's better than nothing. I'll merge this and we can improve it later when we figure out how. |
@davidchambers @joneshf Could you take a look? I'm not sure about wording terminology and such.
Related: fantasyland/fantasy-land#184