-
-
Notifications
You must be signed in to change notification settings - Fork 504
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
Comments
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! |
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 |
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. |
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. |
Adding namespaced functions to any consumable datatypes should work. |
@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? |
@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 |
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? |
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. |
@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 |
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. |
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 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 |
@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)? |
I don't think it solves things like heterogeneous
|
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 |
Well, the libraries do different things. |
Let me summarize some of the conversation here:
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. |
As far as typed functional libraries go, btw, the cutting edge is here: |
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.
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 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. |
Couldn't agree with this more.
This was a mistake... don't repeat it here. |
Could you shed some light on this please? What was the problem? |
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. |
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. |
@SimonMeskens Could you provide a minimum example of what should be done? |
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? |
Do that instead. |
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. |
@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 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) |
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. |
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. |
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? |
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. |
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. |
Sorry for confusion, I meant partial application of course. |
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. |
Huh. Could you explain why uncurried functions would be harder to type? I thought for typing purposes currying actually complicated things. |
@tycho01 Partial application is easier to type than uncurried application. Currying is the hardest of the three. |
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 :( |
Note that there's a lot of confusion about what currying is, the definitions I use are:
I've seen arguments over this, but I find those definitions cause the least amount of it. |
@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? |
I don't think so, can you make one? I don't fully understand the issue, I just have an intuition for it |
@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 In this context, one fair compromise may be to make a version like 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. |
Yeah, I've been wondering how to handle optional arguments in FP. Another common pattern in JS is 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 |
@SimonMeskens: Yeah, definitely,
I just tried this 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? |
You can't rely on 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. |
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.
The text was updated successfully, but these errors were encountered: