Skip to content
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

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open

Add types #281

wants to merge 8 commits into from

Conversation

gabejohnson
Copy link
Member

@gabejohnson gabejohnson commented Jan 11, 2018

This PR is a competitor with #280 and based on a comment from @i-am-tom.

Benefits over #280:

  1. The Fantasy Land organization could provide a library of canonical types which could be used or wrapped by (or not) by conforming libraries.
  2. cata provides a simple, easy to remember interface to the type which provides it.
  3. cata doesn't prescribe any naming convention for conforming libraries to follow.
  4. The cata interface provides a means for conforming libraries to automatically convert between each other.

An example for point 4:

var Maybe = {
  Just: x => ({
    x,
    cata: (_, f) => f(x),
    map(f) { return Maybe.Just(f(x)); }
  }),
  Nothing: {
    cata: (d, _) => d,
    map(f) { return Maybe.Nothing;}
  },
  map: f => m => m.cata(Maybe.Nothing, compose(Maybe.Just, f))
}

var Maybe2 = require('otherlib/maybe');
Maybe.map (f) (Maybe2.Just(1))
     .equals(Maybe.Just(1).map(f))

Drawbacks:

  1. Native types can't be overload with different "views" as mentioned in Proposal: specs for Maybe, Either, Tuple, Task, ... #185 (comment)
  2. cata has a different signature for each type. This could be confusing for newcomers.
  3. No enforced name standardization.

Edit: add point 3 under "Drawbacks"

@gabejohnson
Copy link
Member Author

@davidchambers @robotlolita @Avaq @evilsoft @wavebeem @briancavalier I'm interested in library author/maintainer feedback

@gabejohnson
Copy link
Member Author

/cc @safareli

@gabejohnson
Copy link
Member Author

/cc @gcanti

@gabejohnson
Copy link
Member Author

/cc @paldepind

@wavebeem
Copy link

I don't have any strong opinions on this, but good luck!

@gabejohnson gabejohnson force-pushed the add-types branch 2 times, most recently from 446d13a to 2378198 Compare January 11, 2018 19:27
Copy link
Member

@JAForbes JAForbes left a 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:
Copy link
Member

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?

Copy link
Member Author

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 😄

Copy link
Member Author

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) {
Copy link
Member

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.

Copy link
Member Author

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? */...)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JAForbes this would also be taken care of by the strategy proposed in #280

README.md Outdated
just :: a -> Maybe a
```

A value which conforms to the Maybe specification must provide an `cata` method.
Copy link
Member

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
Copy link
Member

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?

Copy link
Member Author

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}.

Copy link
Member Author

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.

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.

Copy link
Member Author

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.

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.

@briancavalier
Copy link
Contributor

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?

@JAForbes
Copy link
Member

JAForbes commented Jan 12, 2018

@briancavalier sorry if I'm misunderstanding. I think the value that is immediately returned is the Functor. So Promise::cata / Task::cata / Stream::cata could potentially implement the Either type by aliasing bimap to cata.

Someone please correct me if I'm wrong! :D

@Avaq
Copy link
Contributor

Avaq commented Jan 12, 2018

@briancavalier @JAForbes

The challenges are:

  • Dealing with asynchronous values - we cannot coerce something asynchronous to something synchronous, like Either.
  • Dealing with non-determinism - I/O fails "sometimes", a Future might not settle at all.

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 chain, we would need a different constructor which encodes non-determinism (such as the one Promises and Task libraries use), for which I haven't been able to implement cata. Maybe someone can build on what I've reached.

@JAForbes
Copy link
Member

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.

Each function argument to cata must return a value of the same type as cata itself.

That's how Stream::map, Future::map work. So that qualifies.

The Either type encodes the concept of binary possibility (Left a and Right b).

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 value which conforms to the Either specification must provide an cata method.

A Future can do that.

The cata method takes two arguments:

Just like Future::bimap

f must be a function which returns a value

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 undefined. So that might need to be clarified?

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 f and g are called.

Additionally Future.map synchronously returns a Future. So we're all compliant so far.

If f is not a function, the behaviour of cata is unspecified.

This is fine. Libraries can throw if they want. They can have custom functionality.

No parts of f's return value should be checked.

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
Copy link
Member Author

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.

Copy link
Member

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 )

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?

Copy link
Member Author

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.

Copy link
Member Author

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.

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

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.

Copy link
Member Author

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).

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...

@gabejohnson
Copy link
Member Author

gabejohnson commented Jan 12, 2018

@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

@paldepind
Copy link

I like this PR 👍

The Fantasy Land organization could provide a library of canonical types which could be used or wrapped by (or not) by conforming libraries.

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.

@paldepind
Copy link

paldepind commented Jan 12, 2018

@JAForbes

I just want to clarify my point: as the spec above is written, Future meets the Either specification even though a Future is async.

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 f that implements Either a a. I could then do.

const a = f.cata(a => a, a => a);

And I would have to get an a back synchronously.

Edit: Oh, I just saw your comment above. It seems that we agree 😄

@i-am-tom
Copy link

i-am-tom commented Jan 13, 2018

The "value" within a Future is the continuation, right? So, the fold is simply:

cata :: (Continuation -> r) -> r

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.

@briancavalier
Copy link
Contributor

@i-am-tom I think that's the intuition I was looking for. Thanks. Just to make sure I understand: the r is still universally quantified?

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.

@gabejohnson
Copy link
Member Author

/cc @buzzdecafe @CrossEye

@gabejohnson
Copy link
Member Author

I don't want to distract from the general spec work

@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 😄

@CrossEye
Copy link

@gabejohnson

/cc @buzzdecafe @CrossEye

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.

@gabejohnson
Copy link
Member Author

@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.

@alexandru
Copy link

@gabejohnson just some bike shedding ... why not name this function fold instead of cata?

fold is IMO more standard than cata, having been used in many libraries and cata is IMO a little confusing. Yes, I know it comes from "catamorphism".

One thing that bothers me about Fantasy-Land is that it's using naming that I don't recognize. E.g. Setoid instead of Eq, of instead of point or pure (and I'm bothered by this one because it's a waste to sacrifice this name for pure data constructors), chain instead of bind or flatMap, chainRec instead of tailRecM.

Just a minor annoyance, otherwise it's a good proposal.

@safareli
Copy link
Member

@alexandru you might like #280 then, as it proposes to use name of ADT instead of fold or cata, which more reflects runIdentity either maybe form haskell/purescript.

@alexandru
Copy link

@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 maybe and either to have different names.

Here's Scala however:

On the other hand #280 does sound better than cata.

@i-am-tom
Copy link

@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 of, though we more formally mean fantasy-land/of. You're free to write a pure or return method that calls this namespaced function, and you're free to write fold as you've suggested, without any clashes :)

@gabejohnson
Copy link
Member Author

@alexandru I'm a little more partial to #280 because it would allow defining, say either, on an Either type and on a Future/Task type. Or maybe on an Option type and a List type. You could do this with cata too, but you'd have to namespace it somehow (e.g. fantasy-land/either-cata) which amounts to the same thing.

@xaviervia
Copy link

xaviervia commented Feb 19, 2018

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 .either, .maybe, …, functions is more self-documenting and straightforward to understand than having a variadic, polymorphic .cata one.

@alexandru
Copy link

@gabejohnson you’re mentioning above that we could have either defined for Future/Task types, but I cannot see it.

The signature in #280 that I’m seeing cannot be applied to a Future data type:

either :: Either e => e a b ~> ((a -> c), (b -> c)) -> c

The reason is that c would need to be a Future and Either does not have this restriction. Actually there’s exactly one data type that can work with this fold and that is Either.

Ditto for maybe:

maybe :: Maybe m => m a ~> (b, (a -> b)) -> b

Even though it might seem like it, you can’t implement this for a List in a non-confusing way.

I mean OK the left thunk is for the empty list, that’s clear, but is that a the first element? Is it the last? In case of a stream (e.g. Iterator) it might actually make sense for it to be the last because you might want to consume the whole stream. Or heck, it might actually be random.

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 Maybe and nothing else.

The cool thing about folds is that they reflect the shape of the data constructor precisely. That is what makes them a “catamorphism”.

@alexandru
Copy link

@xaviervia

variadic, polymorphic .cata

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 anIO () to be executed after the list gets traversed, for cleaning up resources. Adding such an extra param in “cons” changes “foldr”.

@gabejohnson
Copy link
Member Author

gabejohnson commented Feb 20, 2018

@alexandru

Ditto for maybe:
maybe :: Maybe m => m a ~> (b, (a -> b)) -> b
Even though it might seem like it, you can’t implement this for a List in a non-confusing way.

This is like arguing that there can't be more than a single Applicative instance for List. The vector product variant (ZipList) is just as valid as the scalar (default).

I mean OK the left thunk is for the empty list, that’s clear, but is that a the first element? Is it the last?

It's up to the implementation and should be documented.

The cool thing about folds is that they reflect the shape of the data constructor precisely.

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);

The signature in #280 that I’m seeing cannot be applied to a Future data type

You are correct. Future was a bad example. You would have to restrict c to be ()

Future#either :: Future a b ~> ((a -> ()), (b -> ())) -> ()

@xaviervia
Copy link

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 map2. The implication is that a type, defined structurally from a set of type constructors (Just, Nothing, …) and implemented in programming language that support such type, does not actually exhaust the possible implementations of such type.

The proposal in #280 forces one to face this reality in a way that the .cata implementation does not.

Mind you this does not mean I'm against the .cata proposal: rather that I find the implications of the other one fascinating, and quite consistent with the fact that in JavaScript, a language without static types and type constructors (and consequently, without any canonical way of doing case analysis), function signatures stand out as a natural way of representing type information.

I mean OK the left thunk is for the empty list, that’s clear, but is that a the first element? Is it the last?

Quick note about the many interpretations of .maybe for List: Lists in JavaScript are not recursive types, so there is no reason to think either one is wrong. I think that rather than refuting the utility of the approach, it illustrates that there exists no canonical case analysis in JS, which in my mind gives motivation to not trying to shoehorn one (not that that's what .cata is doing).

@alexandru
Copy link

@xaviervia

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 map2.

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 Setoid or Ord implementations for a single type in the same project, a Map / HashMap implementation is broken.

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 map2 operation, that would be equivalent with the Apply.ap actually and it's a very good example. For an IO / Task data type you could make a map2 operation that does stuff in parallel, instead of being based on chain / bind, which would force sequencing. But in fact, if you have bind defined for a type, then map2 needs to be coherent with it, suspended side effects and all that. And so how we treated this in Cats is to introduce the Parallel type class, also popular in PureScript.

quite consistent with the fact that in JavaScript, a language without static types and type constructors (and consequently, without any canonical way of doing case analysis), function signatures stand out as a natural way of representing type information

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 Fantasy-Land specifying type class operations as instance methods. This makes it next to impossible to define types, because you need F-bounded polymorphism to define interfaces such as Monad as an OOP interface and it isn't a useful interface.

The static-land alternative is much more friendly to types and I would prefer to not depend on JavaScript's dynamic nature going forward.

@alexandru
Copy link

alexandru commented Feb 21, 2018

@xaviervia

Quick note about the many interpretations of .maybe for List: Lists in JavaScript are not recursive types, so there is no reason to think either one is wrong.

Btw, on lists, this isn't about List in JavaScript having a recursive definition.

Well, first of all, JavaScript doesn't have lists, it only has Arrays.

My argument is about maybe not making sense for a list-like data-structure, because that list can signal multiple elements. the signature is confusing because of that and any of the multiple possible implementations is basically useless.

And that's because maybe for a list is next to lawless.

@i-am-tom
Copy link

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

@xaviervia
Copy link

xaviervia commented Feb 21, 2018

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:

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.

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 Fantasy-Land specifying type class operations as instance methods. This makes it next to impossible to define types, because you need F-bounded polymorphism to define interfaces such as Monad as an OOP interface and it isn't a useful interface.

The static-land alternative is much more friendly to types and I would prefer to not depend on JavaScript's dynamic nature going forward.

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.

Btw, on lists, this isn't about List in JavaScript having a recursive definition.

Well, first of all, JavaScript doesn't have lists, it only has Arrays.

My argument is about maybe not making sense for a list-like data-structure, because that list can signal multiple elements. the signature is confusing because of that and any of the multiple possible implementations is basically useless.

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 maybe for an Array, there are several possible valid interpretations, and library authors are free to choose and implement one. The overall point is that a good interpretation of maybe for Array would be useful.

@davidchambers
Copy link
Member

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.

@xaviervia
Copy link

xaviervia commented Jun 18, 2018

@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 🎉

@davidchambers
Copy link
Member

That said, it would be nice to know the rationale for choosing this one over #280 .

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. :)

@gabejohnson
Copy link
Member Author

gabejohnson commented Jun 19, 2018

@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.

@paldepind
Copy link

@gabejohnson

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.

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 cata method. To me, it feels wrong to have many abstractions with overlapping method names. Similarly to how it would be confusing to have many interfaces/type classes with the same method names.

It also has the downside that the fact that an object has a cata method tells very little about what it is. It may have cata because it is an Either or maybe because it is a Maybe. As an example, say I want to implement catMaybes for a list. The function only works on a list of maybes. But, if a user accidentally calls the method on a list of Either's there will be no way for the catMaybes implementation to know as both an Either and a Maybe have a cata method. As a result things will blow up in an less than ideal manner.

Said in another way. With this PR it will be impossible to implement an isMaybe function that returns true for a Maybe and false for an Either. Based on the spec they'll be impossible to tell from each other at run-time. With #280 on the other hand that is easily doable.

@paldepind
Copy link

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:

  • Implementers are given complete freedom as to how they want to implement their Maybe. Any implementation that can work as a Maybe can be used.
  • There a standadized way to construct Maybes.
  • The approach seems to blend a bit better with the Static Land 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.

@masaeedu
Copy link

masaeedu commented Jun 19, 2018

I think simply having cata overloaded without any uniformity to what cata actually is isn't very useful. We should have some object-level representation of the type's shape alongside the constructors and "destructor" (i.e. cata):

// 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 deriving XYZ). For example you can get traversable (and from this, foldable and functor) for many common types simply based on the shape metadata, and this traversable instance corresponds to what is usually manually written by users anyway.

@gabejohnson
Copy link
Member Author

@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.

@gabejohnson
Copy link
Member Author

That looks quite similar to the approach I suggested previously and which was used in #278

@paldepind indeed it does (though slightly more concise). It just took me a while to come around to your perspective 😄

@paldepind
Copy link

At the risk of drawing this decision out even longer, I would encourage you both to submit PRs supporting your proposals.

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?

@masaeedu
Copy link

@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/

@rpominov rpominov removed their request for review August 30, 2020 17:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet