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

Fantasy-land compatibility #204

Closed
SimonMeskens opened this issue Aug 16, 2017 · 46 comments
Closed

Fantasy-land compatibility #204

SimonMeskens opened this issue Aug 16, 2017 · 46 comments

Comments

@SimonMeskens
Copy link
Collaborator

Big fan of the current currying initiative, but it breaks even more compatibility with Fantasy-land I bet. Is there anything we can do? Maybe I should do a PR where I provide namespaced ("fantasy-land/map") properties for everything?

I think Ramda can handle those. They wouldn't be for normal consumption, but for compatibility with duck-typing fantasy-land libraries, like Ramda.

@raveclassic
Copy link
Collaborator

Well, I think currying does not cause compatibility regression because signature of a function is preserved, only the way of applying arguments changes.

Even more sanctuary, which is meant to be fully aligned with fl-spec, provides only curried functions.

And even more, afaik @gcanti stated that fp-ts won't be aligned with fantasy land.

Anyway I believe the PR you are mentioning would be awesome!

@SimonMeskens
Copy link
Collaborator Author

A curried function does not have the same signature as an uncurried one.

Sanctuary doesn't use any curried functions at all for their fantasy-land compliance, so I'm not sure what you're talking about there.

@gcanti stated that aligning with fantasy-land is something we can debate, so I opened this issue

@SimonMeskens
Copy link
Collaborator Author

I'd like to note that the namespaced functions were provided exactly for the issue we're running into and should neatly fix the issue.

@raveclassic
Copy link
Collaborator

Sorry, I mentioned sanctuary from memory and didn't mean to disinform :)

Anyway I'm all for aligning with the existing fl-spec, the question is what we can do to achieve this.

@SimonMeskens
Copy link
Collaborator Author

Adding namespaced functions to any consumable datatypes should work.

@gcanti
Copy link
Owner

gcanti commented Aug 17, 2017

@SimonMeskens @raveclassic AFAIK the ecosystem around FL is fragmented. Before any technical discussion, could you please explain the motivation and the practical benefits of such a compatibility?

@raveclassic
Copy link
Collaborator

@gcanti I don't have any rich experience with FL so my point is mostly because, well, it's a spec and that's all

@SimonMeskens
Copy link
Collaborator Author

I just want to be able to use Ramda and/or Sanctuary to consume the types in fp-ts. Maybe that's expecting a little too much out of TypeScript right now?

@SimonMeskens
Copy link
Collaborator Author

And again, I don't think it should compromise fp-ts in any way, so I'm totally on-board with the current refactor towards currying. If it's a choice between FL compatibility and certain fp-ts features, the fp-ts features win every time.

@gcanti
Copy link
Owner

gcanti commented Aug 17, 2017

I just want to be able to use Ramda and/or Sanctuary to consume the types in fp-ts

@SimonMeskens do you mean consumable by a JavaScript project? I ask because from a TypeScript and in particular from a fp-ts point of view both ramda and sanctuary, being JavaScript libs designed for JavaScript, seem not attractive so maybe I'm missing something

@SimonMeskens
Copy link
Collaborator Author

SimonMeskens commented Aug 17, 2017

Well, Ramda and Sanctuary are functional equivalents of libraries such as lodash and Underscore. It stands to reason that someone will eventually make a TypeScript equivalent (and in fact, @tycho01's Ramda typings are the closest thing to such an effort right now). All of them (except Underscore) work in the same way by using FL to consume datatypes.

I'd say there's not a lot of fragmentation there. The issue right now, is that datatypes are just barely usable in TypeScript, as fp-ts shows, but the libraries that consume them are clearly still too complex to type.

@KiaraGrouwstra
Copy link

the libraries that consume them are clearly still too complex to type

Yeah, on the Ramda TS side there's still a barrier to this interop. To meaningfully calculate return values, Ramda would need to calculate them from the typings of the method implementations. In my ears that implies microsoft/TypeScript#6606.

I ask because from a TypeScript and in particular from a fp-ts point of view both ramda and sanctuary, being JavaScript libs designed for JavaScript, seem not attractive

I believe what you're saying is typings written as separate type definitions are not at par with the inference that full TS libs could. There is truth to that, with !, type guards and expression level constructs not exposed at the type level (function application, rest/spread etc.). I'm not confident that's the argument you're making, but I think the type level reaching feature parity is just a question of time -- and I for one would gladly help push it there.

@SimonMeskens
Copy link
Collaborator Author

@tycho01 Here's the interesting question: do you think we could write a functional library, such as Ramda, today, in TypeScript (aka, some functions may be written differently, to better support type inference, like fp-ts is doing)?

@KiaraGrouwstra
Copy link

KiaraGrouwstra commented Aug 17, 2017

I don't think it solves things like heterogeneous map. I'm trying to fix the TS compiler though. The number of TS issues I care about are actually somewhat limited:

  • 6606-type-level-function-application
    • 12424 (comment) evaluation order issue
  • 5453-variadic-kinds:
    • capture ... params as if they were regular (array) params
    • [...a]
    • fn(...args)
  • 16072 generics erased
  • 6229-known-length-tuples (WIP at Make tuples have known length microsoft/TypeScript#17765)
  • 17086 type fails in function

@gcanti
Copy link
Owner

gcanti commented Aug 17, 2017

some functions may be written differently

Indeed. @tycho01 is doing an awesome job with npm-ramda and typical, however most of the problems simply vanish if you design the APIs for TypeScript in the first place. fp-ts is first of all a TypeScript library, written in TypeScript and for TypeScript users. Also it's mostly a porting of PureScript libraries and requires pretty standard type system features. The super hacky part is faking HKTs

@KiaraGrouwstra
Copy link

Well, the libraries do different things.
Like Lodash, Ramda is largely about operating on the built-in array/object structures, meaning the vast majority of their functions involve iteration -- whether to map, reduce or filter.
For that purpose, the inference on the standard library currently only goes half-way, that is to say, won't give granular inference even if you did have statically known inputs.
Doing ADTs is definitely kinda different in that respect.

@SimonMeskens
Copy link
Collaborator Author

Let me summarize some of the conversation here:

  • Fp-ts shouldn't compromise to fit Fantasy-land when it comes to currying
  • There is a simple way, using namespaced ("fantasy-land/*") functions to add compatibility at any point
  • There are currently no consuming libraries that reach the typed standard of fp-ts
  • Once there are such libraries, efforts should be made to ensure compatibility, preferably through FL

Is that fine for everyone involved? My recommendation is to leave this issue open (label as low-priority?) for now and follow-up on it. If I have some spare time, I might PR some of the namespaced functions, just for completion's sake, but I don't think it's a priority. I think spending time on a fully typed Ramda alternative would be more sensible in that case.

As far as Static-land goes btw, there's a similar easy solution, but I don't even know of any libraries that try to consume static-land at all.

@SimonMeskens
Copy link
Collaborator Author

As far as typed functional libraries go, btw, the cutting edge is here:
https://github.com/TylorS/typed (he has several other libraries called typed-*)
https://github.com/frptools

@KiaraGrouwstra
Copy link

There are currently no consuming libraries that reach the typed standard of fp-ts

It's kinda apples-to-oranges, but once TS can handle it that part would need to be incorporated on the consuming (Ramda) side, yeah.

I think spending time on a fully typed Ramda alternative would be more sensible in that case.

Let me get you started with the major challenges the Ramda typings are facing then, left open with some notes in the issues (though I didn't discuss there much as few other people seemed active in this type-level space anyway):

I imagine a pure TS lib could already type maybe a binary version of pipe plus stuff about concatenating tuples. The rest of the issues still stand I fear -- I specifically asked about the latter two on the TS issue list, and know the TS team was considering the first two as examples in microsoft/TypeScript#5453. If anyone finds a way, hopefully we'd learn about it. But if even the language creators don't see a way, you might conclude this is no longer about getting experience or learning arcane secrets -- the language just doesn't support them yet. Not that we're far -- I believe 6606 + 5453 solves just about anything.

If this were a silver bullet though, then it would probably become worth it to petition Ramda to integrate. Last time we discussed that I hadn't considered this vital, so we'd decided against it for the moment to both retain freedom of release schedule.

I did check the libs; the approaches currently taken look similar.

@SimonRichardson
Copy link

Fp-ts shouldn't compromise to fit Fantasy-land when it comes to currying

Couldn't agree with this more.

There is a simple way, using namespaced ("fantasy-land/*") functions to add compatibility at any point

This was a mistake... don't repeat it here.

@raveclassic
Copy link
Collaborator

@SimonRichardson

This was a mistake... don't repeat it here.

Could you shed some light on this please? What was the problem?

@SimonRichardson
Copy link

It was originally intended to solve that issue that people didn't want to name squat on the names; map, chain, bimap, etc. I've come to the conclusion that was just pointless, it made things more verbose, more complex for what really amounted to zero sum.

It's an Ivory Tower.

@SimonMeskens
Copy link
Collaborator Author

I disagree. Unless Fantasy-land specifies that all methods are properly curried, there's no way to pick the right variation. The main incompatibility between fp-ts and FL is exactly that, for example, some functions being applied differently. We could fix this at the cost of making either fp-ts worse or lobbying all existing consumer libraries, both terrible solutions, or you could just optionally implement namespaced functions.

You might think it's pointless, but I'm actually implementing this support for Collectable, where it's an essential feature that fixes real issues (there, there's a different issue with tree-shaking, that is fixed by them). In Fp-ts, likewise, it provides a real solution to the issues faced.

So instead of an ivory tower, I'm noticing that the two libraries I'm involved with can fix their issues exactly due to that change.

@raveclassic
Copy link
Collaborator

@SimonMeskens Could you provide a minimum example of what should be done?

@SimonMeskens
Copy link
Collaborator Author

I'm not currently able to open a dev environment, so this might have some syntax errors, but it should explain the idea:

class MyType {
  private ["fantasy-land/map"](transformer: any) {  }
  map() // the correct fp-ts map implementation

Things to note: the namespaced one is set to private and thus hidden, we don't actually want to expose it. Its behavior is exactly what fantasy-land specifies and it's not typed, because it doesn't need to be. In most cases, it can just directly call the fp-ts implementation.

The fp-ts one is public, correctly typed and might be curried differently. This means the type can make use of the typing advantages of fp-ts, but it can also be passed along to a consuming library, which will just duck-type for the existence of the namespaced one and use that one instead of the slightly non-compliant version. From testing, this should work with Ramda, and if any consuming library doesn't, it should probably be PRed, because prioritizing the namespaced function is what Fantasy-Land specifies.

Does this make sense or do you want a more rigorous example?

@SimonRichardson
Copy link

Unless Fantasy-land specifies that all methods are properly curried

Do that instead.

@SimonMeskens
Copy link
Collaborator Author

It would massively complicate all libraries trying to implement it, degrade performance for certain libraries and make every existing FL-supporting library completely useless. There are other issues too. The namespaced solution is actually quite elegant and I'm not sure why you don't like it? I've started using similar namespacing schemes in other projects of mine, because I enjoy the idea of namespaced functions as interfaces so much.

That said, if there were a good solution to the curry problem, a proper fix would indeed be a good thing to have. Ah well, too late for that now, can't redo the spec, it would break everything.

@raveclassic
Copy link
Collaborator

raveclassic commented Nov 8, 2017

@SimonMeskens Thanks for the explanation, makes sense.

But honestly IMO I don't think fp-ts core is a right place for such compat layer. This looks like a perfect fit for a separate package like fp-ts-fl-compat with helpers that patch known constructor prototypes once included into the bundle:

import { FantasyFunctor } from 'fp-ts/lib/Functor'
import { HKTS, HKT2S } from 'fp-ts/lib/HKT'
import { Some, some } from 'fp-ts/lib/Option'

const FL_MAP = 'fantasy-land/map'

type Ctr<T> = {
  new (...args: any[]): T
}

function patchFunctor<F extends HKTS | HKT2S, A>(Target: Ctr<FantasyFunctor<F, A>>): void {
  Target.prototype[FL_MAP] = function map<B>(this: FantasyFunctor<F, A>, f: (a: A) => B) {
    return this.map(f)
  }
}

patchFunctor(Some)
const a = some(2)
const double = (n: number): number => n * 2
console.log((a as any)[FL_MAP](double)) // some(4)

@SimonMeskens
Copy link
Collaborator Author

Oh for sure, I agree this could be done in a separate package. Not quite sure how patching works with tree-shaking, but that's a discussion for another time. In any case, the namespacing is what allows this freedom and they allow us to worry less about FL.

@raveclassic
Copy link
Collaborator

Also it would be great to see where exactly fp-ts diverges from FL, can't remember it now. Perhaps such places could be aligned if the use case is more common.

@SimonMeskens
Copy link
Collaborator Author

The problem was that there are a few cases where uncurried functions can't be typed properly, so the choice was made to switch to full partial application everywhere to offer better type support. FL on the other hand is fully uncurried and untypable in TS as it stands because of this.

I haven't had the chance yet to check what you guys did with the currying. I assume instead of fully currying, where any combination of partial application works, there was a switch to partial application everywhere?

@raveclassic
Copy link
Collaborator

Recalling #200 currying was introduced for the sake of ergonomics when using compose/pipe. The problem was with type constraint arguments, so some types of some functions like traverse (type constraints go first) or free map (type constraint goes last also as a real argument) couldn't be properly inferred. So current approach is mixed - curry everything where possible.

@SimonMeskens
Copy link
Collaborator Author

Please define "curried". Curried, to me, means you can choose how to apply. For example, some code I wrote yesterday:

let mult = (a, b, c) => a * b * c;
assert("curry changes length recursively", curry(mult)(2).length, mult.length - 1);
assert("curry does partial application", curry(mult)(2)(3)(5), 30);
assert("curry does full application", curry(mult)(2, 3, 5), 30);
assert("curry does mixed application 1", curry(mult)(2, 3)(5), 30);
assert("curry does mixed application 2", curry(mult)(2)(3, 5), 30);

As you can tell, you can call it whichever way, in terms of arguments. That form of currying is compatible with FL right now of course, but a function you can only call with partial application (the second assert there) is not.

@raveclassic
Copy link
Collaborator

Sorry for confusion, I meant partial application of course.
Well, that's quite interesting. Seems like currying (partial/mixed/full application) is a bit trickier and requires original function to be uncurried which leads to where it all started in #200 - reducing api duplication.

@SimonMeskens
Copy link
Collaborator Author

Yeah, hence why I suggest using untyped, private namespaced functions (in a separate package sound fine). There's one other solution of course, which is to properly curry everything and only type it as partial, but that seems messy, not to mention it degrades performance for no reason.

@KiaraGrouwstra
Copy link

The problem was that there are a few cases where uncurried functions can't be typed properly, so the choice was made to switch to full partial application everywhere to offer better type support.

Huh. Could you explain why uncurried functions would be harder to type? I thought for typing purposes currying actually complicated things.

@SimonMeskens
Copy link
Collaborator Author

@tycho01 Partial application is easier to type than uncurried application. Currying is the hardest of the three.

@SimonMeskens
Copy link
Collaborator Author

Here's the example that shows this:

import { Applicative } from '../src/Applicative'
import { HKT } from '../src/HKT'
import { option, some } from '../src/Option'
import { right } from '../src/Either'

// curried version
declare function traverse1<F>(F: Applicative<F>): <A, B>(f: (a: A) => HKT<F, B>, ta: Array<A>) => HKT<F, Array<B>>

// x: HKT<"Option", number[]>
export const x1 = traverse1(option)(a => some(a), [1, 2, 3])
// export const x2 = traverse1(option)(a => right(a), [1, 2, 3]) // error :)

// uncurried version
declare function traverse2<F, A, B>(F: Applicative<F>, f: (a: A) => HKT<F, B>, ta: Array<A>): HKT<F, Array<B>>

// x3: HKT<"Option" | "Either", number[]>
export const x3 = traverse2(option, a => right(a), [1, 2, 3]) // no error :(

@SimonMeskens
Copy link
Collaborator Author

Note that there's a lot of confusion about what currying is, the definitions I use are:

  • Uncurried: a single application step, arguments are treated as an input tuple ((a, b, c) => int)
  • Full partial application (or just curried): every argument is applied separately ((a) => (b) => (c) => int)
  • Fully curried: the function allows both and might even allow mixing ((a, b) => (c) => int works, as well as other permutations)

I've seen arguments over this, but I find those definitions cause the least amount of it.

@KiaraGrouwstra
Copy link

@SimonMeskens: thanks for illustrating, that was illuminating. I don't think I'd seen similar inference issues outside of the ADT context yet. Had anyone filed an issue on this over at TS yet so far?

@SimonMeskens
Copy link
Collaborator Author

I don't think so, can you make one? I don't fully understand the issue, I just have an intuition for it

@KiaraGrouwstra
Copy link

@SimonMeskens microsoft/TypeScript#19871

@KiaraGrouwstra
Copy link

@SimonMeskens: on currying / partial application variants, I think I've just come across another type that appears to be useful for languages like JS that allow optional arguments, although it might not be worthy of a special label.

Many JS functions are kinda like (a, b, c?, d?) => ..., where (importantly) the first argument is the main input data, followed by perhaps some required and maybe then some optional arguments.

In this context, one fair compromise may be to make a version like (b, c?, d?) => (a) => .... This nets the benefit of still creating a reusable function for use in e.g. composition, while preserving the convenience of optional arguments.

I'll concede this is less relevant for libraries made with FP in mind; it's rather relevant for creating pointfree versions of existing JS methods/functions that were not.

@SimonMeskens
Copy link
Collaborator Author

Yeah, I've been wondering how to handle optional arguments in FP. Another common pattern in JS is ( { a, b, c } ) => ... with destructuring. This allows optional and default values too.

The problem here is probably that JS is based on some form of powerful object calculus (Cardelli expresses a nice one in A Theory of Objects) and FP is still very much rooted in lambda calculus. While both are equally powerful, it's sometimes hard to translate from one to the other.

I have some plans to write a library in the future that does some of this work for you, allowing you to lift object constructs into a functor/monad/lattice/kitchen sink that you can then use FP techniques on.

For example, I assume you can express optional values as some sort of state/maybe hybrid monad wrapping a lambda for example. A lift function that takes something like (a, b, c?, d?) => ... and turns it into MaybeState<(b) => (a) => ...> would be handy. It would need a map function that lets you apply the lambda to functions, a way to alter the provided state and then some sort of fold to actually apply the function and return a value. I'm not sure if that's how you would do it in Haskell, I'm not knowledgeable at all about idiomatic Haskell.

@KiaraGrouwstra
Copy link

@SimonMeskens: Yeah, definitely, ( { a, b, c } ) => ... seems to be what you'll end up doing if you design your surface with FP in mind.

I have some plans to write a library in the future that does some of this work for you, allowing you to lift object constructs into a functor/monad/lattice/kitchen sink that you can then use FP techniques on.

I just tried this pointfree based on the (...) => (a) => ... approach, like bluebird's promisifyAll fixing Node's callback API, except here to yield nice 1-arity versions of all legacy non-FP JS functions for use in pointfree style (in function composition, mapping, etc.). I tried proposing it for Ramda too, but we'll see how that plays out.

On the state monad thing, if I understand correctly the point is to allow passing/altering arguments in different orders despite the underlying function being optional-based?
That'd sound not unlike turning it into that unary object style, which sounds interesting, though I'm not sure how to elegantly accomplish that.
You'd wanna like read the names of arguments from a function, while optional args are hidden even in Function.prototype.length; you'd have to like toString the function and parse out the parameter names from there, which feels a bit hacky. :(

@SimonMeskens
Copy link
Collaborator Author

You can't rely on length in the case of optional arguments, so when lifting, you'd have to explicitly explain what the arguments to the function look like unfortunately. You can sometimes get away with relying on length though. I currently have this in my library:

const curry = (fn, length, ...applied) => {
  const partial = (...args) =>
    (length || fn.length) <= (applied.length + args.length)
      ? fn(...applied, ...args)
      : curry(fn, (length || fn.length), ...applied, ...args);

  Object.defineProperty(partial, "name", { get: () => fn.name });
  Object.defineProperty(partial, "length", { get: () => (length || fn.length) - applied.length });

  return partial;
};

const member = fn => {
  const member = function(...args) {
    return fn(...args, this);
  };

  Object.defineProperty(member, "name", { get: () => fn.name });
  Object.defineProperty(member, "length", {
    get: () => (fn.length > 0 ? fn.length - 1 : 0)
  });

  return member;
};

// Test curry
let assert = (message, value, check) => {
  const log = value === check ? console.warn : console.error;
  log(`${message}: ${value === check} (${value} === ${check})`);
};
let mult = (a, b, c) => a * b * c;
assert("curry changes name", curry(mult).name, mult.name);
assert("curry changes length", curry(mult).length, mult.length);
assert(
  "curry changes length recursively",
  curry(mult)(2).length,
  mult.length - 1
);
assert("curry does partial application", curry(mult)(2)(3)(5), 30);
assert("curry does full application", curry(mult)(2, 3, 5), 30);
assert("curry does mixed application 1", curry(mult)(2, 3)(5), 30);
assert("curry does mixed application 2", curry(mult)(2)(3, 5), 30);

Might still have some bugs. It's cute, but I'm not sure how useful it is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants