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

Change the spec so it would require prefixed method names #92

Closed
rpominov opened this issue Apr 26, 2015 · 98 comments
Closed

Change the spec so it would require prefixed method names #92

rpominov opened this issue Apr 26, 2015 · 98 comments

Comments

@rpominov
Copy link
Member

I propose to change the spec so it would require more unique method names. For example, instead of ap method, a @@fantasy-land/ap. It can be called as foo['@@fantasy-land/ap']().

This would serve two purposes:

  1. It will be easier to add spec support to an existing API, as method names will be guaranteed not taken,
  2. we could use duck-typing to check if an object implements a spec.

This also will allow to create a polyfill that adds fantasy-land support to native JS data structures. It relatively ok to add @@fantasy-land/ap method to Array.prototype, but less ok to add ap.

Also libraries like Bacon, for example, will be able to add fantasy-land support, as for now it problematic with map method, for instance, as it not strictly compatible with the spec, but they could add @@fantasy-land/map that is.

I think this change could significantly speed up the adoption of fantasy-land specification.


The idea inspired by transducers protocol, seems like it worked out for them pretty well.

See also discussion on Gitter: https://gitter.im/fantasyland/fantasy-land?at=553cb82d20328f114ca36f0f

@CrossEye
Copy link

👍

This would make life much easier for Ramda which is a generic functional programming library for JS, but which will work well with types implementing the FantasyLand spec:

    R.map(square, [1, 2, 3]); //=> [1, 4, 9]
    R.map(square, Just(5)); //=> Just(5).map(square); //=> Just(25)
    R.map(square, Nothing); //=> Nothing.map(square); //=> Nothing
    R.map(square, Right(7)); //=> Right(7).map(square); //=> Right(49)
    // etc.

While we do this sort of dynamic dispatching on a number of our functions, there is a special place in our hearts for the FantasyLand specification, and I would love to see this work more generally than it does now. We could easily code to dispatch to such FL versions first if they're defined and after that to matching named function. This would mean that Ramda might integrate better with Bacon and other libraries that have conflicts with the FL names.

@SimonRichardson
Copy link
Member

Why not just put the fantasy-land library in a wrapper and expose your new
method names. I just don't feel fantasy-land should do this IMO.

So 👎 from me.

On Sat, 23 May 2015 20:47 Scott Sauyet notifications@github.com wrote:

[image: 👍]

This would make life much easier for Ramda
https://github.com/ramda/ramda which is a generic functional
programming library for JS, but which will work well with types
implementing the FantasyLand spec:

R.map(square, [1, 2, 3]); //=> [1, 4, 9]
R.map(square, Just(5)); //=> Just(5).map(square); //=> Just(25)
R.map(square, Nothing); //=> Nothing.map(square); //=> Nothing
R.map(square, Right(7)); //=> Right(7).map(square); //=> Right(49)
// etc.

While we do this sort of dynamic dispatching on a number of our functions,
there is a special place in our hearts for the FantasyLand specification,
and I would love to see this work more generally than it does now. We could
easily code to dispatch to such FL versions first if they're defined and
after that to matching named function. This would mean that Ramda might
integrate better with Bacon and other libraries that have conflicts with
the FL names.


Reply to this email directly or view it on GitHub
#92 (comment)
.

@CrossEye
Copy link

The problem is that FantasyLand wants to claim ownership of all of these names:

  • equals
  • concat
  • empty
  • map
  • ap
  • of
  • reduce
  • sequence
  • chain
  • extend
  • extract

With the exception of ap (and arguably concat), these are very common English words, with many meanings.

If a user has a type that uses one of these words for another meaning -- map for something geographic, reduce for fractions, extend for credit processing, etc. -- then she cannot make her type a Functor, a Foldable, an Extend, etc., according to the FantasyLand specification, without renaming her perfectly-well named functions.

With the suggestion here, she could easily add the FantasyLand types, and any tool that uses the types in a generic way and any algorithm written against these algebraic data types would work as they do today, But we gain a more declarative sense too. Right now, the existence of a chain method on an object certainly does not imply that that object also has a map method, as this specification would like. Because clearly the existence of the chain method could be for some other purpose. But if it had @@fantasy-land/chain, one could certainly infer that compliance with this specification is intended, and then code accordingly. (I would actually prefer something like @@algebraic-data-types/chain or @@alg-types/chain, but that's just bikeshedding.)

@puffnfresh
Copy link
Member

On the naming: Array's map should allow it to be a Functor. Patching monkeys to add support is not something I'm in favour of.

@CrossEye something with chain doesn't have to have map:

A value that implements the Chain specification must also implement the Apply specification.

A value which satisfies the specification of an Applicative does not need to implement: (map)

@SimonRichardson
Copy link
Member

(function (e) {
  var fl = require ("fantasy-land-library");
  // require all the fantasy land libraries here.
  // you could use common js everywhere if you're using the browser
  ...
  e['@@fantasy-land/ap'] = ap;
  ...
})(whatever);

That way you're not exposing any fantasy-land functions that you don't
want. We could even have a git repo full of these.

On Mon, 25 May 2015 05:20 Brian McKenna notifications@github.com wrote:

On the naming: Array's map should allow it to be a Functor. Patching
monkeys to add support is not something I'm in favour of.

@CrossEye https://github.com/CrossEye something with chain doesn't have
to have map:

A value that implements the Chain specification must also implement the
Apply specification.

A value which satisfies the specification of an Applicative does not need
to implement: (map)


Reply to this email directly or view it on GitHub
#92 (comment)
.

@rpominov
Copy link
Member Author

On the naming: Array's map should allow it to be a Functor.

Right, but we could also add @@fantasy-land/ap, @@fantasy-land/chain, @@fantasy-land/of etc. to Array.prototype. Not sure if it makes much sense in case of Array, but what about native Promise, for instance?

@rpominov
Copy link
Member Author

Also the spec can be stricter this way, e.g. it can demand that @@fantasy-land/map(fn) must call fn with a single argument, or in all places where it says that a behavior is undefined, it can say that an exception must be thrown.

Also 👍 on @@alg-types/chain, @@fantasy-land/chain is just something first came to my mind, and we needed to call it somehow 😄

@CrossEye
Copy link

@SimonRichardson: Perhaps I'm not being very clear. Ramda is not trying to implement the specification at all. (Well, there is a side project, but that's not the point) Instead, Ramda is presenting a more functional API centered around functional composition. Many of its functions work on lists, and some of these overlap with the FantasyLand methods. For these and some others we do a form of dynamic dispatch. Thus R.map(fn, object) will call the object's map method if it exists, passing fn. But currently we also have no guarantee that the map we call has anything to do with the expected Functor behavior. For all we know, it could be geography.

@puffnfresh:

@CrossEye something with chain doesn't have to have map

Yes, sorry, but the point should still be clear.

I really like the way the transducers protocol has been handled as a low level library that's out of the users' way, and which will not interfere with other design of their tools. I would love to see FL do the same.

@joneshf
Copy link
Member

joneshf commented May 25, 2015

I think what would benefit this proposal would be more clarity in what is suggested. It's kind of hard to understand what the point is, who it benefits, what would change, how to mitigate confusion, and what current libraries would need to do.

It sounds like the proposal can help foster adoption, but it's hard to see how.

@SimonRichardson
Copy link
Member

@CrossEye I might reluctantly give in on this, but I really think that as long as this is an alias to the functions then it could work. I absolutely think that the functions should stay as is and that your namespaced aliases should just be sugar in the spec.

I use fantasy-land by hand and I do know what map does in my code base and definitely don't want to use ['@@fantasy-land/map'] as functions in my code base.

@rpominov
Copy link
Member Author

@joneshf

I think what would benefit this proposal would be more clarity in what is suggested.

I'll try to summarize.

what would change

Only names of the methods that the specification require to implement.

what current libraries would need to do

Also only change the names.

who it benefits

  1. Libraries like Ramda, that want to add methods that operates on types described in FL. Because duck-typing will be more reliable — if an object has @@fantasy-land/map, it's for sure implements FL Functor.
  2. Libraries that want to implement FL. Because they may already use short names, that FL now requires, for something else, or for same functionality but with slightly different API, that incompatible with the spec.
  3. We will be able to add FL methods as polyfills to native classes, which automatically allow libs like Ramda to operate on a native Promise as on a Monad, for example.
  4. The FL spec will be able to become stricter e.g., when it says that a behavior is undefined, it can say that an exception must be thrown. It will improve reliability in all the code that uses FL one way or another. For instance, we won't need to worry about stuff like ['10', '10', '10'].map(parseInt) // [10, NaN, 2].

@SimonRichardson

I use fantasy-land by hand and I do know what map does in my code base and definitely don't want to use ['@@fantasy-land/map'] as functions in my code base.

Good point! But I don't see a problem here, all libs that implement FL, will be able to add both long and shorter name with the same functionality. They even can state in the docs that they "implement FL with both long and shorter names". Also you'll can use libs like Ramda instead of directly call FL methods, if you like, but again no one stops anybody to add methods with short names and use them directly.

@CrossEye
Copy link

@SimonRichardson

I might reluctantly give in on this, but I really think that as long as this is an alias to the functions then it could work. I absolutely think that the functions should stay as is and that your namespaced aliases should just be sugar in the spec.

Unfortunately, this would likely only work if it were the other way around, if users implemented @@alg-types/map. They could feel free to implement map too as an alias if they like, but generic tools, which know only about the Functor specification would be looking for the former. This would be something akin to the iteration spec of ES6: the language can perform, for instance, for-of on anything that has the correct symbol defined. But that symbol is not one that has any significant chance of colliding with other uses.

@paldepind
Copy link

I'm highly in favor of making this change. The arguments given so far has been splendid but there is another angle as well: Currently fantasy land requires data types to implement a sensible object oriented API. It completely wrong for a specification that is coming from a functional angle.

I've created a library that exposes reactive streams that implements the applicative interface. This means that the streams must have a map method. I've only added the map method to support fantasy land. My users should not use it directly, they should instead use the map function I make available. However since object oriented programming is so common in JavaScript people continue to get it wrong even though I've added a disclaimer in the documentation. This confusion would probably never happen if the method names where prefixed.

It is very annoying that one has to implement a object oriented API that can easily be mistaken for a genuine OO API in an otherwise completely functional library that makes no use of methods.

@puffnfresh

On the naming: Array's map should allow it to be a Functor. Patching monkeys to add support is not something I'm in favour of.

Array.prototype.map is not a proper map function. It invokes the mapping function with three arguments which wreaks havoc on curried functions. Not having a collision with Arrays misbehaving map would be an advantage, not the opposite.

@paldepind
Copy link

@SimonRichardson

I use fantasy-land by hand and I do know what map does in my code base and definitely don't want to use ['@@fantasy-land/map'] as functions in my code base.

That is of course easily solvable by using a function like this function map(fn, app) { return app[''@@fantasy-land/map'](fn); }. Then you could also curry the function and additionally avoid the troubles with methods in JavaScript (worrying about their this binding).

@SimonRichardson
Copy link
Member

@paldepind I vote for you to make a wrapper and expose what you need to :)

@paldepind
Copy link

@SimonRichardson How would a wrapper help expect making it more cumbersome to use my library with consumers of fantasy land adts? That is not a solution.

I am not satisfied that the only counter argument in this discussion has been "I just don't feel fantasy-land should do this IMO." This is not about feelings. This is about making it easier and less problematic for libraries to expose adts and for consumers of adts to be safe and certain that they are dealing with the real thing. And not any of the thousand unlawful map implementation that you will find in the wild west of JavaScript.

@CrossEye
Copy link

@SimonRichardson:

It's clearly understandable why this spec was written the way it was. There is a real desire for this to simply work where possible with existing code. Use map instead of fmap since Arrays are already Functors using map. Use object methods instead of pure functions since they're nearly ubiquitous in JS, and avoid (almost!) any necessity for type dictionaries. Give users familiar names, since they're easier to type and to understand.

But this can break down eventually for reasons already expressed. People really would like to describe their types according to the FantasyLand specification, and use them with tools that are ready to work with Functors, or Applicatives, or whatever. But because of competing concerns in their domain, they can't really use the names reserved by FantasyLand for their Functor or Applicative methods. Or they have preexisting functions with similar behavior but a different API that their users are already far too long accustomed to use. Suddenly FantasyLand's easy solution is not so simple.

Users can get around this only by writing wrappers for any type that has these problems, a horrible solution. Libraries that work with these types cannot get around this at all. But FantasyLand has a simple fix. It's worked well for Transducers and for the Iteration protocol; it really seems like a good solution here too.

@SimonRichardson
Copy link
Member

I've only added the map method to support fantasy land. My users should not use it directly, they should instead use the map function I make available.

@paldepind why bother at all, if map does what it's supposed to do F[A] -> F[B] then what's the issue. You're exposing a very useful method for people to use. If the implementation doesn't work how your library works, then just don't align to the specification. I don't see the need to align to the specification at all if you don't want your library to expose map.

But FantasyLand has a simple fix. It's worked well for Transducers and for the Iteration protocol;

@CrossEye I would agree, but fantasy-land isn't doing anything magical here. It's not setting up things for mutability nor is it doing anything grossly underhanded. I see no point in it! The methods in the specifications are really simple, there is no ambiguity about what happens and when it happens. If you want to add a fantasy-land protocol then maybe that's the way to move forward. I just don't see the need to make something that is inherently simple, complicated.

Fantasy-sorcery implements a generic map method and I've used this many times in the past without issue, again, maybe it's worth moving forward with this.
https://github.com/fantasyland/fantasy-sorcery/blob/master/index.js#L16-L22

It might actually help if the spec was versioned, then this could be 2.0.0. (or somehow represent a divergency)? Ultimately we could put this to a vote and see what the outcome is, I maybe fighting against the tide here :)

@paldepind
Copy link

@CrossEye I very much agree with you. But I have to complain about this again:

Use map instead of fmap since Arrays are already Functors using map.

This is a similar claim to what @puffnfresh wrote earlier. But it is still only superficially true.

Consider the following completely normal (albeit contrived) usage of an applicative

var multiply = curry(function(x, y) { return x * y; });
function multWith(functor) {
  return functor.map(multiply);
}

Here fn should obviously return a functor of functions. But if you pass an array to it that will not be the result – you will break it. Pretending that Array is a functor is wishful thinking that is only going to cause trouble. Ideally fantasy land should add prefixes and specify that functions passed to map must only be invoked with one single argument. Otherwise fantasy land compatible applicatives can and will be completely unreliably as is the case right now.

@paldepind
Copy link

@SimonRichardson

why bother at all, if map does what it's supposed to do F[A] -> F[B] then what's the issue. You're exposing a very useful method for people to use.

You can say that. But the point is that it causes confusion for users. They see the map method (which is only there for fantasy land but they don't now that) and think they should use it. Then they go look for other methods but they don't see them (expect for the other fantasy land methods I implement). Then they get confused. Anyway, for my library this it not a huge issue. I mostly broad it up since other hadn't. For other libraries that already have an incompatible map method the problems are much bigger (as @CrossEye and @rpominov have explained). Also the array issue I explained above is more significant as well.

@SimonRichardson
Copy link
Member

For other libraries that already have an incompatible map method

Then it's not fantasy-land compliant. It's that simple.

@rpominov
Copy link
Member Author

@SimonRichardson Just wonder, what is the purpose of fantasy-land in your opinion? Please, don't get me wrong, it's a serious question, not a troll. It's just seems like we see the purpose of this spec differently, which is the source of all the confusion about this proposal.

@paldepind
Copy link

Then it's not fantasy-land compliant. It's that simple.

Of course? Don't you see that the problem arises as soon as someone wants that library to become compliant?

@SimonRichardson
Copy link
Member

@rpominov I didn't take it as such :)

It's just seems like we see the purpose of this spec differently, which is the source of all the confusion about this proposal.

I 💯 agree with you about this, it seems like libraries are wanting to align to the specification, which is completely understandable. I just don't see the reason why you would want to hide various methods because it might confuse people. If you think your library might confuse people by how map is implemented when aligned to the specifications then either don't align to it or educate people on how it's implemented and why.

From my point of view, I would like to code to the specification without additional complexity, like I can already do now. I'm all for progressive changes to the specification, but I don't see the need for the namespaced complexity as map, chain, extend, extract, etc are so easy to implement in their current guise.

@paldepind

My users should not use it directly, they should instead use the map function I make available.

And not any of the thousand unlawful map implementation that you will find in the wild west of JavaScript.

Adding another map to the wild west isn't helping either, as you're doing the thing which is confusing, as you're saying you're compliant with fantasy-land and then you're not at the same time. Consistency is key, when implementing a library for end users?

@CrossEye
Copy link

But I have to complain about this again:

Use map instead of fmap since Arrays are already Functors using map.

This is a similar claim to what @puffnfresh wrote earlier. But it is still only superficially true.

I agree. I was speaking of the reasons the specifcation was written is what is on the surface the easiest manner, but which is in the end more complex than is necessary.

Note, though, that your example depends on a style of curry than many in the FP community might not recognize nor accept. By many definitions, and in many implementations, a curried function is a function of one parameter.

@CrossEye
Copy link

For other libraries that already have an incompatible map method

Then it's not fantasy-land compliant. It's that simple.

And because FantasyLand has chosen to define itself in terms of common words such as 'map', 'empty', 'reduce', 'chain', etc., such users can't make their libraries tantasy-land compatible. Maybe others feel this is fine, and only those that can throw aside whatever other constraints they have upon them are worthy of implementing this spec. But I feel it's a shame. I would rather these ideas were easier for users to implement.

@SimonRichardson
Copy link
Member

But I feel it's a shame. I would rather these ideas were easier for users to implement.

@CrossEye

Ultimately we could put this to a vote and see what the outcome is, I maybe fighting against the tide here :)

@SimonRichardson
Copy link
Member

If people vote that the spec should be namespaced, then I think semver should be pushed through at the same time, then at least we can live together.

@rpominov
Copy link
Member Author

What if we publish a fantasy-land package to npm. The package will expose constants:

const fl = require('fantasy-land')
fl.map === 'fantasy-land/map' // we may use Symbols with this approach as well, I think

So the libraries that use FL in any way will add that package as a Peer Dependency with as wide versions range as possible. And use constants that the package exposes as method names in any place they use FL spec.

For example we may have a library that provides map() that may be used with arrays and any FL Functors:

// my-map-lib/index.js

const fl = require('fantasy-land')

exports.map = (fn, target) => {
  if (Array.isArray(target)) {
    return target.map(fn)
  }
  if (target && target[fl.map]) {
    return target[fl.map](fn)
  }
}

// my-map-lib/package.json
{
  "peerDependencies": {
    "fantasy-land": "*"
  }
}

Then we may have a library that provides some type that implements Functor:

// my-id-lib/index.js

const fl = require('fantasy-land')

exports.Id = function(x) {
  this._x = x
}
exports.Id.prototype[fl.map] = function(fn) {
  return new exports.Id(fn(this._x))
}

// my-id-lib/package.json
{
  "peerDependencies": {
    "fantasy-land": "*"
  }
}

And finally we can use both of libs in our app code:

// my-app.js

const {map} = require('my-map-lib')
const {Id} = require('my-id-lib')

const foo = new Id(1)
const bar = map(x => x + 1, foo)

bar._x // 2

// package.json
{
  "dependencies": {
    "my-id-lib": "1.0.0",
    "my-map-lib": "1.0.0",
    "fantasy-land": "1.0.0"
  }
}

Backward compatibility for free

What is good about this approach is that we can publish first version of fantasy-land that exposes unprefixed names, so all current libraries will be automatically compatible with that first version.

So if I want to access FL methods directly in my code, I can install fantasy-land@1.0.0 plus some lib that haven't updated yet and use them like this:

// my-app.js

const {map} = require('fantasy-land')
const {Id} = require('old-lib')

const foo = new Id(1)
const bar = foo[map](x => x + 1)

bar._x // 2

// package.json
{
  "dependencies": {
    "old-lib": "1.0.0",
    "fantasy-land": "1.0.0"
  }
}

This will work just fine, although won't save us from name clashes. So then we can publish fantasy-land@2.0.0 that exposes prefixed names. And consumers will have a choice to use 1.0.0 and get compatibility with all libs, or use 2.0.0 and get compatibility only with libs that migrated to new approach + name clashes safety.

@rpominov
Copy link
Member Author

Actually @robotlolita has suggested exactly that. Sorry I didn't re-read comments before writing this.

So the Cons was:

  • Someone still needs to guarantee that only one fantasy-land-interface module gets loaded for the entire program. npm doesn't help with this;
  • Realms still break things.

I think peer dependencies solve the first, and we shouldn't have any problems with Realms if we use strings and not Symbols (don't know why we should use Symbols anyway).

@SimonRichardson
Copy link
Member

Create a PR and then we can see what it feels like to use :)

On Sun, 17 Jan 2016, 17:58 Roman Pominov notifications@github.com wrote:

Actually @robotlolita https://github.com/robotlolita have suggested
exactly that. Sorry I did't re-read comments before writing this :)

So the Cons was:

  • Someone still needs to guarantee that only one
    fantasy-land-interface module gets loaded for the entire program. npm
    doesn't help with this;
  • Realms still break things.

I think peer dependencies solve the first, and we shouldn't have any
problems with Realms if we use strings and not Symbols (don't know why we
should use Symbols anyway).


Reply to this email directly or view it on GitHub
#92 (comment)
.

@SimonRichardson
Copy link
Member

I've had a play with it and it works for me. I'm happy to merge this one in - @puffnfresh you happy with this?

@SimonRichardson
Copy link
Member

This is now merged 👍 Shall we close and open a new issue discussing namespaced prefixes in the future?

@rpominov
Copy link
Member Author

That's great! 🎉 🎊

Yeah, a new issue would be nice, this one a bit too long and hard to read.

@rpominov
Copy link
Member Author

rpominov commented Apr 3, 2016

One point were made here by me and others is that with unique prefixed names we can tell for sure if an object has a compliant FL method. But it doesn't mean it has an algebra (doesn't mean that the method obeys certain laws).

For example if an object have chain method it does't mean it has Monad algebra. Strictly speaking we can't even say that it has Chain algebra, because spec requires methods to be defined in order for type to support algebras but not vise versa.

I realized it just recently and thought it worth mentioning here.

@CrossEye
Copy link

CrossEye commented Apr 3, 2016

One point were made here by me and others is that with unique prefixed names we can tell for sure if an object has a compliant FL method. But it doesn't mean it has an algebra (doesn't mean that the method obeys certain laws).

I think there's no way to tell that in general. This is not even a Javascript issue; it must be fairly fundamental and related to issues like the halting problem and Rice's Theorem.

The advantage of prefixed names is that with them it becomes abundantly clear that someone is meaning to create types that adhere to the laws for the given algebra. Code that tries to do something generically against any sort of Monoid can comfortably use your type if you have a prefixed empty and a prefixed concat. It really can't make that assumption just because you happen to have those (unprefixed) methods.

@rpominov
Copy link
Member Author

rpominov commented Apr 3, 2016

Yeah, I wouldn't worry too much about Monoid case, but chain for instance is more problematic, we can't really tell if a type has Monad or just Chain.

The problem seems solvable though, at least in JavaScript. We could require a property on each value with the list of algebras it implements. E.g. obj[fl.algebras] = ['Functor', 'Chain', 'Monad'].

@CrossEye
Copy link

CrossEye commented Apr 4, 2016

Ok, I thought you were worried about a different problem.

Oh, yes I'm sure it's possible to have something be both a Chain and and Applicative without satisfying, or even attempting to satisfy, the identity laws. (Does anyone have a good example of this?)

I'm still in favor of a TypeRegistry version instead. But I don't know when, or at this point even if, I'll get back to my attempt at this.

@puffnfresh
Copy link
Member

@CrossEye ap should be able to be derived by:

function ap(f, ma) {
  return f.chain(function(x) {
    return ma.map(function(a) {
      return x(a);
    });
  });
}

I don't have a proof, but I have a feeling that Monad identity laws can be shown to follow from the Applicative and Bind laws given the above.

@CrossEye
Copy link

CrossEye commented Apr 4, 2016

@CrossEye ap should be able to be derived...

Yes, that's in the spec.

I don't have a proof, but I have a feeling that Monad identity laws can be shown to follow from the Applicative and Bind laws given the above.

I'd be very very interested to see such a proof if anyone knows one.

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

No branches or pull requests