fantasyland / fantasy-land

Specification for interoperability of common algebraic structures in JavaScript
MIT License
10.08k stars 373 forks source link

Add types #281

Open gabejohnson opened 6 years ago

gabejohnson commented 6 years ago

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 https://github.com/fantasyland/fantasy-land/issues/185#issuecomment-356500208
  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 commented 6 years ago

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

gabejohnson commented 6 years ago

/cc @safareli

gabejohnson commented 6 years ago

/cc @gcanti

gabejohnson commented 6 years ago

/cc @paldepind

wavebeem commented 6 years ago

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

briancavalier commented 6 years ago

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 commented 6 years ago

@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 commented 6 years ago

@briancavalier @JAForbes

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 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 commented 6 years ago

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.

gabejohnson commented 6 years ago

@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 commented 6 years ago

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 commented 6 years ago

@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 commented 6 years ago

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 commented 6 years ago

@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 commented 6 years ago

/cc @buzzdecafe @CrossEye

gabejohnson commented 6 years ago

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 commented 6 years ago

@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 commented 6 years ago

@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 commented 6 years ago

I've updated #280 to more clearly contrast with this proposal.

alexandru commented 6 years ago

@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 commented 6 years ago

@alexandru you might like https://github.com/fantasyland/fantasy-land/pull/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 commented 6 years ago

@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 commented 6 years ago

@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 commented 6 years ago

@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 commented 6 years ago

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 commented 6 years ago

@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 commented 6 years ago

@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 commented 6 years ago

@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 commented 6 years ago

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 commented 6 years ago

@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 commented 6 years ago

@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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

@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 commented 6 years ago

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 commented 6 years ago

@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 https://github.com/fantasyland/fantasy-land/issues/185#issuecomment-358989533.

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 commented 6 years ago

@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 commented 6 years ago

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.

masaeedu commented 6 years ago

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 commented 6 years ago

@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 commented 6 years ago

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 commented 6 years ago

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 commented 5 years ago

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