-
Notifications
You must be signed in to change notification settings - Fork 376
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
Add types #281
base: master
Are you sure you want to change the base?
Add types #281
Conversation
@davidchambers @robotlolita @Avaq @evilsoft @wavebeem @briancavalier I'm interested in library author/maintainer feedback |
/cc @safareli |
/cc @gcanti |
/cc @paldepind |
I don't have any strong opinions on this, but good luck! |
446d13a
to
2378198
Compare
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'm so happy about this proposal. Thanks for taking this on @gabejohnson. I really like @i-am-tom's approach
Marking as Approve because these are more suggestions than anything else.
|
||
In Fantasy Land, types are specified by an object containing a catamorphic | ||
method `cata`. `cata` has an arity matching the number of constructor functions | ||
belonging to each type. Each argument to `cata` is either: |
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.
For the benefit of people who don't know the difference between Types and Type Classes, I think we should elaborate the difference here to avoid confusion. Maybe a paragraph explaining the benefits in having a specification for both?
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 had considered placing the type specifications in a separate file but figure that could be a separate PR.
I agree that an introductory paragraph contrasting the specification of algebras with that of types would be useful. Suggestions are welcome 😄
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 think it might be helpful to have a section describing sum and product types.
Though I don't consider this specification a particularly good resource for JS devs new to FP, many find themselves here early on.
} | ||
|
||
// Alternatively | ||
Id.prototype['fantasy-land/cata'] = function cata(f) { |
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.
When prefixing, should we also include the type name (e.g. fantasyland/either-cata
) to allow libraries to customize their dispatch and provide type errors for incompatible types? E.g. bimap
over Maybe
could fail.
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.
Are you talking about:
S.cata(Id.of(1), /* How do I check the arity of this function? */...)
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.
README.md
Outdated
just :: a -> Maybe a | ||
``` | ||
|
||
A value which conforms to the Maybe specification must provide an `cata` method. |
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.
Should an cata
should be a cata
?
README.md
Outdated
1. If `g` is not a function, the behaviour of `cata` is unspecified. | ||
2. No parts of `g`'s return value should be checked. | ||
|
||
### Tuple |
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.
Should we support tuples with more than two values?
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.
Tuple3 a b c
is isomorphic to Tuple a (Tuple b c)
or Tuple a (Tuple b (Tuple c Unit))
. Also, I'd rather not complicate things by adding a specification for each Tuple{n}
.
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.
A general pattern for the definition of tuples could be written and derivations from Tuple
provided, but that should be a separate PR.
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.
What about renaming Tuple
to Pair
? That seems more precise since this is a 2-tuple i.e. a pair.
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.
@paldepind there's an implementation of Tuple
for Sanctuary called Pair
https://github.com/sanctuary-js/sanctuary-pair/tree/gabejohnson/everything 😄
I simply referred to Tuple
here because it's the name I'm most familiar with.
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.
To me, the word "tuple" is more general. It covers both 1-tuples (singletons), 2-tuples (pairs) 3-tuples, etc. However this specification is not for tuples in general—it is for 2-tuples in particular. So I think naming it "pair" would be more appropriate.
Interesting. Using Church encoding as a specification for fantasyland types? I like that a lot. One thing I don't know much about (perhaps oddly) is how Church encoding extends to asynchronous data types whose cata can't really return a value synchronously. I'd be interested to learn more about that, since the FL-compliant libraries I maintain are async. Anyone have any info or links? |
@briancavalier sorry if I'm misunderstanding. I think the value that is immediately returned is the Functor. So Someone please correct me if I'm wrong! :D |
The challenges are:
I played around with a catamorphism that deals with the first challenge of asynchronousity. It has a pretty crazy signature, and I'm not sure if it's a valid catamorphism, and it seems to depend on deterministic constructors, but it's implementable. Future a b = { cata :: ((((a -> ()) -> ()) -> ((c -> ()) -> ())), (((b -> ()) -> ()) -> ((c -> ()) -> ()))) -> ((c -> ()) -> ()) }
rejected a :: ((a -> ()) -> ()) -> Future a b
resolved b :: ((b -> ()) -> ()) -> Future a b This is an implementation: const Future = {
rejected: run => ({
cata: (f, _) => f (run),
map: _ => Future.rejected (run),
fork: (f, _) => run (f)
}),
resolved: run => ({
cata: (_, f) => f (run),
map: f => Future.resolved (cont => run (x => cont (f (x)))),
fork: (_, f) => run (f)
}),
map: (f, m) => m.cata (
Future.rejected,
run => Future.resolved (cont => run (x => cont(f (x))))
)
}; And it seems to work: const m = Future.rejected (f => f (1)) // Reject with 1
.map (x => x + 10) // Mapping gets ignored
.cata (Future.resolved, Future.rejected) // We can flip using catamorphism
.map (x => x + 1) // Now mapping works
Future.map (x => x + 1, m) // Map using catamorphism
.fork (console.error, console.log) // Final value is 3 EDIT: But to implement |
Sorry @rpominov I'm not trying to detract from your observations I think they may be far more precise technically or in the broader context of FP literature. I just want to clarify my point: as the spec above is written, Future meets the Either specification even though a Future is async. Maybe that means the spec has to change to better represent catamorphisms? But as far as I can tell, from what's written above, an async library could support Either's cata without breaking spec.
That's how Stream::map, Future::map work. So that qualifies.
Even if a Future has a 3rd state: unresolved, or even a 4th state like cancelled. Future does have 2 states, and so it can support this specific requirement. A library is free to implement those 2 states as any of those 4. There's also no requirement or clarification in the spec about the life cycle of these states, it's unspecified. All that matters so far is we return the same type, and we meet arity requirements.
A Future can do that.
Just like
Are we specifying that a developer has a return statement or uses an arrow function? Because all JS functions return a value, even if the value is Maybe this is the part that seems synchronous. I don't think it does because there's nothing in the spec that specifies how or when the cata visitors Additionally Future.map synchronously returns a Future. So we're all compliant so far.
This is fine. Libraries can throw if they want. They can have custom functionality.
Also has no bearing on asynchronicity. The spec as written is wide open. If that's technically inaccurate then we might need to make the spec more specific, but as it's written, as far as I can tell: async types qualify. |
README.md
Outdated
method `cata`. `cata` has an arity matching the number of constructor functions | ||
belonging to each type. Each argument to `cata` is either: | ||
|
||
1. a value |
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.
This should read "a value which matches the return type of cata
itself." or something to that effect.
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.
Yeah that would nullify everything I said ( which is good! :D )
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.
Ah! That sentence confused me. But, what about removing it altogether? JS is a dynamic language and if people want to do aMaybe.cata("foo", (n) => 12)
why should we stop them?
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 would say the user should convert the Maybe
to an Either
:
// annotate :: a -> Maybe b -> Either a b
annotate = x => m => m.cata(Left(x), Right);
annotate("foo", aMaybe).cata(x => x, n => 12)
While cata
will certainly be used in application code (it exists after all) I would presume implementing libraries would provide more ergonomic methods/functions.
I'd like to keep static type checkers (TS, Flow, etc.) in mind when specing these types out.
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.
We could remove it and just roll this case into point 2 by requiring a thunk instead of a value. That way we could accommodate potentially expensive computations.
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 would say the user should convert the
Maybe
to anEither
That only seems like a more roundabout way of doing the same thing and now you just have a "problematic" cata
invocation on an Either that in one case returns a string and in another a number?
I'd like to keep static type checkers (TS, Flow, etc.) in mind when specing these types out.
Agreed, but both options can be type checked in both TS and Flow.
My point is just that from the point of view of the cata
implementation it doesn't matter what the functions return (cata
is not allowed to inspect it). There doesn't seem to be any benefit to having that requirement in the spec.
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.
That only seems like a more roundabout way of doing the same thing and now you just have a "problematic" cata invocation on an Either that in one case returns a string and in another a number?
There's nothing stopping the user from using the method in the manner you describe.
I'm going to remove the sentence anyway. I think a function should always be passed in (a thunk in this case).
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.
The spec is covered in types - if people want to do Just(10).cata("foo", id)
, they're welcome to do so, but they're not fantasyland-compliant...
2378198
to
15281ac
Compare
@Avaq with this definition things become a bit less hairy: () = Undefined
EffectFn a = (a -> ()) -> ()
Future a b = { cata :: (EffectFn a -> EffectFn c, EffectFn b -> EffectFn c) -> EffectFn c }
= Either (EffectFn a) (EffectFn b)
rejected a :: EffectFn a -> Future a b
resolved b :: EffectFn b -> Future a b |
I like this PR 👍
How much value would such a library add? Given how easy these methods are to define (as per your example) I personally would not be interested in adding a dependency just to avoid writing a few very simple methods. |
Please correct me if I'm wrong, but I really don't see how that could be the case. Let's say I have a Future const a = f.cata(a => a, a => a); And I would have to get an Edit: Oh, I just saw your comment above. It seems that we agree 😄 |
The "value" within a
You don't want to coerce it, and you certainly don't want to unpack it. You just want to pass around the value that represents that continuation. I would also say that this isn't a type that needs specifying - you can use IO to represent asynchronous actions via callbacks, and Future can be seen as an abstraction over the callbacks. Easiest way to solve the problem is to avoid it :D EDIT: I think the larger problem here is the assumption that you can always unpack monads, which is in conflict with its laws! If I want an "unpackable" IO, what I actually probably want is a DSL. |
@i-am-tom I think that's the intuition I was looking for. Thanks. Just to make sure I understand: the Thanks for help in understanding. I don't want to distract from the general spec work--just want to make sure it'll be possible to represent async types nicely. |
/cc @buzzdecafe @CrossEye |
@briancavalier not at all. These questions are the reason I've invited so many lib authors to review this PR. It would be nice to have everyone implementing this spec to be on the same page 😄 |
I've been following along. I don't have much to add at the moment. I haven't yet figured out what it would mean for Ramda. |
@CrossEye I guess I should look at the watchers list before spamming people 😊 I suppose it doesn't have much of an impact on Ramda ATM. |
@gabejohnson just some bike shedding ... why not name this function
One thing that bothers me about Fantasy-Land is that it's using naming that I don't recognize. E.g. Just a minor annoyance, otherwise it's a good proposal. |
@alexandru you might like #280 then, as it proposes to use name of ADT instead of fold or cata, which more reflects |
@safareli you're referring to maybe and either. Note that Haskell and PureScript do not do OOP-like namespacing of functions and they don't do function overloading either, so they need Here's Scala however: On the other hand #280 does sound better than |
@alexandru it's worth mentioning that none of the names you've given are the actual fantasy-land-assigned names. For those unfamiliar with the fantasy-land spec: https://github.com/fantasyland/fantasy-land/blob/master/index.js Colloquially, we refer to methods like |
@alexandru I'm a little more partial to #280 because it would allow defining, say |
The reason I particularly like #280 is precisely because it effectively adds an extra feature that vanilla pattern matching does not have in other languages. The example that I know of which behaves similarly are Idris' views; and I would say that rather than "pattern matching over the type constructors", a better description of the proposal in #280 would be "add the ability to pattern match on views of the type". JavaScript being a non-statically typed language, it makes intuitive sense to me to embed type information in function signatures: it is the natural way of verifying type data in JS. In this sense, having |
@gabejohnson you’re mentioning above that we could have The signature in #280 that I’m seeing cannot be applied to a either :: Either e => e a b ~> ((a -> c), (b -> c)) -> c The reason is that Ditto for maybe :: Maybe m => m a ~> (b, (a -> b)) -> b Even though it might seem like it, you can’t implement this for a I mean OK the left thunk is for the empty list, that’s clear, but is that But then in either case this means that you’re either dropping elements on the floor, or you’re calling that function multiple times. Nothing makes sense here actually, that signature is made for The cool thing about folds is that they reflect the shape of the data constructor precisely. That is what makes them a “catamorphism”. |
Same argument as above, I would argue that for any data type there’s a single fold definition that reflects the shape of the data constructors and that is useful. And you can have other folds definitions only if the data type is a subset of another data type. List is a wonderful example: foldr :: (a -> b -> b) -> b -> [a] -> b This function basically reflects the shape of “cons”, first param, along with the shape of “nil”, second param. Yes, you can implement this for Maybe, b/c Maybe can be seen as a one element list, being a subset. Not that useful. It’s also worth mentioning that if you add params or data constructors to List’s definition, then this fold no longer works 😉 For example say you added an |
This is like arguing that there can't be more than a single
It's up to the implementation and should be documented.
I agree. But I also think there are useful implementations for other data structures. // Either a b -> Maybe b
const hush = e => e.maybe(Nothing, Just);
// Maybe b -> a -> Either a b
const annotate = (d, m) => m.either(K(Left(d)), Right);
You are correct. Future#either :: Future a b ~> ((a -> ()), (b -> ())) -> () |
The interesting underlying topic is that there are many possible implementations of a certain algebra for a certain type, which is sometimes taken into account by calling a second possible implementation of Functor The proposal in #280 forces one to face this reality in a way that the Mind you this does not mean I'm against the
Quick note about the many interpretations of |
That's not a very good example. In general type class usage comes with a coherence requirement meaning that for a given type you can't have more than one instance of a given type-class. This is one of the problems when working with type classes and the TL;DR is this: If you don't have coherence, polymorphic code making use of type classes is probably screwed — for example if you have multiple Basically type classes don't work without coherence. Without coherence you're better off with ML modules, which can be emulated via OOP. Edward Kmett can probably do better in explaining this concept: https://youtu.be/hIZxTQP1ifo Speaking of a
Speaking of this, JavaScript isn't necessarily dynamically typed. Some of us are pushing for TypeScript or Flow at a minimum, which can add a types layer on any JS library that can be quite helpful. I'm not very happy with The |
Btw, on lists, this isn't about Well, first of all, JavaScript doesn't have lists, it only has Arrays. My argument is about And that's because |
Are all folds not lawless? There's no guarantee in the list fold that I'm actually going to walk the whole thing? I think this is getting a little off-topic now, so I'm going to unwatch and leave y'all to it. <3 |
Mind that I did not write "type-class", but "algebra". Type-classes are just a language specific approach of representing algebras containing a set and an operation. There is definitely more than one way of implementing an algebra with a specific set: one per each operation which satisfies that algebra. Simple example: the integers form at least two monoids, one with addition and 0 and the other one with multiplication and 1. I personally regard the fact that mainstream FP languages can only implement one at a time as a disadvantage, but that's besides the point: the main thing is that I was not talking about type-classes, so map2 does remain a relevant example. I don't disagree with the rest of your point, I think you are actually right; what I wanted to focus on though was in the fact that having multiple possible implementations of an algebra with a type it's an overlooked fact that the #280 proposal brings to light in an interesting way.
Sure! The thing is, those dialects are not JavaScript. I don’t want to get technical about what is JavaScript and what is not: what I mean is that it does seem the Fantasy Land philosophy is to be a pure JavaScript specification, and one based on functions to that. I don’t fully understand why is the proposal of instance methods an inconvenience in this particular case and not in general though, since FL is already based on instance methods. As a small sidetrack, I don’t think FL should be restricted because of Flow or TS concerns. Flow and TS already don’t support higher-kinded polymorphism and I hit this issue head first when trying to type check a project with a lot of higher-order functions. Given that those type systems are not powerful enough to cover Haskell-like type classes, I don’t see how they can be catered for anyway.
Yes, sorry, I was in automatic mode. About the "not making sense": I think that might be too strong a statement. Even if there is not an universal way of making sense of |
There's support for this proposal. Are you interested in picking it up again, @gabejohnson? If so, I will leave comments for some minor changes I would like to see. |
@davidchambers I think both this and #280 are great and will be much welcome by the community, so I'm not interested in delaying this further. That said, it would be nice to know the rationale for choosing this one over #280 . Not interested in restarting the discussion :D just in having a statement of the reasons, it might be useful for explaining this to others and have more insight into the goals and priorities of the Fantasy Land project. Looking forward to have this merged and start using it 🎉 |
I don't think a decision has been made. There's broad support for merging one or other pull request; I believe @gabejohnson, having given the matter the most thought, is best positioned to decide which pull request should be merged. :) |
@davidchambers I'm still very interested in adding types to the specification. I'm still somewhat torn though between this proposal, #280, and an option mentioned by @robotlolita in #185 (comment). As I've become more familiar with row types over the past several months, I've become interested in the idea of specifying records and variants instead of named product and sum types. type Maybe a = { Just: a } | { Nothing: null }
type Tuple a b = { fst: a, snd: b } This would form would be trivial to serialize and would fit well with https://github.com/tc39/proposal-pattern-matching if it ever sees light. |
That looks quite similar to the approach I suggested previously and which was used in #278. Specifically the encoding was: type Maybe a = { isJust: true, value: a } | { isJust: false } The two differ mostly in the details. With regards to this PR and #280 I've come to prefer #280. This PR adds many different types all of which have a It also has the downside that the fact that an object has a Said in another way. With this PR it will be impossible to implement an |
There is another idea that has crossed my mind that may be worth considering. ADTs have two parts to them: Ways to construct them and ways to deconstruct them. Or, at the type level, they have introduction rules and elimination rules. However, the approaches that we've discussed so far only deal with destructuring of the types. What would a spec that captured both aspects look like? Here is one example (expressed as TS types): type MaybeSpec<A> = {
just: (a: A) => Maybe<A>;
nothing: () => Maybe<A>;
match<B>: (maye: Maybe<A>, justCase: (a: A) => B, nothingCase: () => B) => B;
} This spec says that an implementation of a Maybe is a module (in the Static Land sense) that contains a function for constructing a just, a funtion for constructing a nothing, and a function that performs pattern matching/case analysis on a Maybe. Here are some of the benefits to the above approach:
Here is one simple implementation of the spec: const ArrayPoweredMaybe = {
just: (a) => [a],
nothing: () => [],
case: (maybe, just, nothing) => maybe.length === 0 ? nothing() : just(array[0])
} With such a specification any library that wants to use a Maybe can be completely parametrized over the Maybe implementation. Here is a small example: function myArrayLib(maybeImpl) { // parametized over the maybe implementation
return {
find: (predicate, array) => {
const idx = array.findIndex(predicate);
return idx === -1 ? maybeImpl.nothing() : maybeImpl.just(array[idx]);
},
head: (array) => array.length === 0 ? maybeImpl.nothing() : maybeImpl.just(array[0]),
removeNothings: (array) => array.filer((maybe) => maybeImpl.match(maybe, () => false, (_) => true))
};
} I hope the idea is clear. Any library parametized over the Fantasy Land Maybe spec could work with any maybe implemention. Such libraries can not only deconstruct Maybe's they can also construct them. |
I think simply having // a shape is a function that accepts a factory of symbols and produces
// a structure recursively consisting of objects, arrays, and other shapes
const maybeShape = ({ a }) => ({ Just: [a], Nothing: [] })
const listShape = ({ a }) => ({ Nil: [], Cons: [a, listShape] }) This gives us several useful things, such as the ability to generate common instances simply based on the shape of the type (same as you can get in Haskell with |
@paldepind @masaeedu both of these approaches appear to have merit 😄 At the risk of drawing this decision out even longer, I would encourage you both to submit PRs supporting your proposals. Perhaps then we could open a meta-issue to discuss and attempt to achieve consensus. |
@paldepind indeed it does (though slightly more concise). It just took me a while to come around to your perspective 😄 |
I think that is a good idea. I'll have time to do so after my finals. @masaeedu Your idea sounds very interesting. I don't fully understand it though. Could you maybe explain it in a bit more details? Would the object-level representation of a type's shape exist in addition to a destructuring function or as a replacement for it? |
@paldepind Sorry about the long delay in getting back to you, I kept putting off implementing the idea. Here's a rough sketch that demonstrates what I'm talking about: with access to sufficient structural information about all ADTs, it is possible to have a single implementation for serializing any ADT value (including for nested and recursive ADTs): https://jsfiddle.net/y73zfd2u/ |
This PR is a competitor with #280 and based on a comment from @i-am-tom.
Benefits over #280:
cata
provides a simple, easy to remember interface to the type which provides it.cata
doesn't prescribe any naming convention for conforming libraries to follow.cata
interface provides a means for conforming libraries to automatically convert between each other.An example for point 4:
Drawbacks:
cata
has a different signature for each type. This could be confusing for newcomers.Edit: add point 3 under "Drawbacks"