-
Notifications
You must be signed in to change notification settings - Fork 41
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
Reference to a canonical module in values #45
Comments
👍 on the proposal. Having modules to pass around is great, however type-class instances are meant to be coherent and |
Ah, one small suggestion — I don't necessarily like that Consider this type: class Box {
constructor(value) { this.value = value }
equals(that) { return that && this.value === that.value }
}
const BoxModule = {
equals: (x, y) => x.equals(y)
} I would rather pass around Also, I would rather have it as a function (thunk), to avoid any declaration problems when the "module" is defined separate from the class: class Foo {
constructor(value) { this.value = value }
equals(that) { return that && this.value === that.value }
static ["static-land/canonical"] = () => FooModule
}
const FooModule = {
equals: (x, y) => x.equals(y)
} |
Thank you for the suggestions! I agree that making // @flow
type Maybe<T> =
| {tag: 'some', value: T}
| {tag: 'nothing'}
const MaybeModule = {
of<T>(x: T): Maybe<T> {
return {tag: 'some', value: x, 'static-land/canonical': MaybeModule}
},
map<A, B>(f: A => B, x: Maybe<A>): Maybe<B> {
if (x.tag === 'nothing') {
return x
}
return MaybeModule.of(f(x.value))
}
} I really like this style, and Flow seems to promote using it via its documentation https://flow.org/en/docs/types/unions/#toc-disjoint-unions . To limit us to use of Classes would be a big loss. Also not sure about the thunk, we could always write it like this: class Foo {
constructor(value) { this.value = value }
equals(that) { return that && this.value === that.value }
}
const FooModule = {
equals: (x, y) => x.equals(y)
}
Foo.prototype["static-land/canonical"] = FooModule
// or, if we want to use static property:
// Foo["static-land/canonical"] = FooModule And from the semantics perspective, using thunk would suggest that Although maybe adding a property later like this may cause issues with TypeScript, Flow, etc. Maybe to use thunk is a good idea indeed. |
Would this still be possible? Could one define of<T>(x: T): Maybe<T> {
- return {tag: 'some', value: x, 'static-land/canonical': MaybeModule}
+ return {tag: 'some', value: x, constructor: {'static-land/canonical': MaybeModule}}
}, |
Yes, from a strictly technical perspective both locations (
|
BTW, I think we should use
|
In the context of FP, in which we are, that cannot be — a By not making it a thunk the API is imposing an evaluation strategy, which in JavaScript in particular is a pretty serious problem due to the order of evaluation. On the naming change, here's my 2 cents:
Ease of understanding is irrelevant for this particular field, because this field is not meant for users, being middleware — or in other words it is solely for library authors that want to build generic code making use of constrained parametric polymorphism, making use of those type classes, in which case they've got bigger problems than understanding the purpose of this field. Also in my mind "module" as a word is not such a good name, because in other languages at least it is a synonym for namespaces. Yes, I know of ML modules, but ML doesn't do type classes and in the context of type classes these are type class instances 😉 I don't mind "module", I'm just saying that for me at least "module" is not necessarily clarifying anything — but then again I don't really care, it's just a name, you can use whatever you want.
I think that particular issue is a red herring btw:
And for instance the issue mentions I have a library at https://github.com/funfix/funfix and I'm rebooting the described type classes to be based on But to get back to the point, The problem with regular properties such as And given that something like |
Right, but in JavaScript world this might still send a wrong signal. Although we could explain this in specification as clear as possible, which should help. There is a similar case with the
I was thinking of a person not familiar with static-land reading source code of a library that implements say Maybe.
But yes, "module" will only confuse people who're not familiar with this specification 😅 Regarding enforcing name to be string literal, I'm also not too concerned with the particular issue I've linked to. I just worry we might have issues that we don't anticipate right now. That was the case with that issue with Flow, when we added namespaces to Fantasy Land. It's only an example of unanticipated issue. Didn't know about problem with Google Closure, this seems like a good argument for enforcing literals! |
Aren't the following two technically similar?
And aren't therefore the following two also similar?
function lift2(f, a, b) {
const T = a['static-land/canonical']
return T.ap(T.map(a => b => f(a, b), a), b)
} But this generic function canonicalLift2(f, a, b) {
const ap = f.constructor['fantasy-land/ap']
const map = f.constructor['fantasy-land/map']
return ap.call(b, map.call(a, a => b => f(a, b)))
}
function coercedLift2(T, f, a, b) {
const ap = T['fantasy-land/ap']
const map = T['fantasy-land/map']
return ap.call(b, map.call(a, a => b => f(a, b)))
} Nobody ever uses it like shown in Unless I'm missing something, it seems to me that this change puts Static Land in exactly the same place as Fantasy Land. The one major difference being that Static Land has much better communication, as you seemed to suggest:
Is this indeed the intent of this change? To gain the same capabilities that Fantasy Land has, but improve upon the way it's communicated? Or did I miss something, and are the capabilities different somehow? |
Yeah, good question, @Avaq ! What are we trying to do here, we already have Fantasy Land that can do dynamic dispatch. I have concerns like "will it split the community?", "will it put more burden on library authors, having to think about two specifications?", etc. I don't know the answers, and trying to be very cautious — just opened an issue to discuss this so far. My reasoning is the following: this will make Static Land technically as capable as Fantasy Land while keeping all advantages of Static Land (https://github.com/rpominov/static-land#pros). So overall we might end up with a more capable specification. But does it worth it? I don't know.
I don't think people will have to use two variations at the same time. A person should choose do they want to pass around modules (use |
I don't think having the ability to operate on different instances is useful at all. Type classes work reliably only when you can count on their coherence, meaning you're only allowed to have one instance of a type class per type in the whole project. For example if you can't rely on a global equality or ordering notion for a given type, then your generic Edward Kmett touches on the importance of coherence in this presentation, doing a better job than I could: https://youtu.be/hIZxTQP1ifo The actual advantage that |
Thank you for your replies. I'm really trying to gain an understanding of how Static Land and Fantasy Land differ fundamentally, especially after this change.
I want to go over these advantages:
This actually changes with the introduction of
I always thought this is what set Static Land apart, but according to @alexandru, we cannot have this (unless I misunderstood, see below). Furthermore, this does not apply when working with dynamically dispatched abstractions.
Except that they cannot have the
But when they choose module passing, they are using Static Land pre-#45. When they choose dynamic dispatch, they are essentially using Fantasy Land.
What you're saying here makes sense. I must've misunderstood something. In the past @joneshf wrote about the ability to have multiple implementations of a type class' instance on the same type, using as an example
I always thought this is simply because of the lack of support for higher kinded types. Eg
I don't see how Static Land brings a solution to this problem. |
It's not the lack of higher-kinded types. funfix and fp-ts are using an encoding for HKTs of which I ranted about, due to TypeScript 2.7 breaking it, but that's fixable — as long as the language supports generics, then this is possible too. The problem is actually the co/contra-variance of functions that arises naturally from inheritance (and usage of TL;DR — the definitions are incompatible with the notion of single dispatch, which is what you get with Fantasy-Land. The native description of interface Chain<A> {
chain<B>(f: A => Chain<B>): Chain<B>
} But this is wrong, because you can't do this: class Box<A> extends Chain<A> {
chain<B>(f: A => Box<B>): Box<B>
} Should be obvious why, but it's because functions have contra-variant behavior in their parameters. Older versions of TypeScript might not complain, but TypeScript isn't a sound language. And the second reason for why the contra-variant behavior here is entirely correct is because different monadic types don't compose. For example this makes no sense whatsoever: promise.chain(_ => list)
list.chain(_ => promise) The only language I worked with and that allows for a correct
Here's how it looks like, translated to TypeScript, if TypeScript actually supported HTKs AND self types and self-recursive types: interface Chain<A, Self<A> extends Chain<A, Self>> { self: Self<A> =>
chain<B>(f: (a: A) => Self<B>): Self<B>
} It is obvious that interface Monad<A, Self<T> extends Monad<T, Self>> { self: Self<A> =>
chain<B>(f: (a: A) => Self<B>): Self<B>
companion: MonadBuilders<Self>
}
interface MonadBuilders<F<A> extends Monad<A, F>> {
of<A>(a: A): F<A>
} Now compare all that crap, which is in fact only useful for implementation inheritance (used for Scala's collections, but with mixed results), with this: interface Monad<F> {
chain<A, B>(f: (a: A) => F<B>, fa: F<A>): F<B>
of<A>(a: A): F<A>
} Which is actually what we have right now in Funfix/fp-ts, with the HTKs encoding: interface Monad<F> {
chain<A, B>(f: (a: A) => HK<F, B>, fa: HK<F, A>): HK<F, B>
of<A>(a: A): HK<F, A>
} |
So you were able to implement HTKs encoding for Static Land because it does not use //static land dynamic dispatch
function map (f, m) {
const dispatch = m['static-land/canonical'].map
return dispatch (f, m)
}
//fantasy land dynamic dispatch
function map (f, m) {
const dispatch = m['fantasy-land/map']
return dispatch.call (m, f)
}
//static land static dispatch
function map_ (T, f, m) {
const dispatch = T.map
return dispatch (f, m)
}
//fantasy land static dispatch
function map_ (T, f, m) {
const dispatch = T['fantasy-land/map']
return dispatch.call (m, f)
} |
No, I just described F-bounded polymorphism and |
@Avaq to more precisely define the problem — as said above, function parameters are naturally contra-variant, whereas So you can't define a method on an interface that gets more restrictive in implementing types, as it should be with It's easy btw to miss this when working in a dynamic language, because everything is implicit, so I encourage you to play around with TypeScript or with Scala and go through the exercise of defining such a |
So the way I see it now is that after this change, Static Land will have the same capabilities as Fantasy Land with regards to dispatching, but will be superior in several ways:
This all seems to me like a great thing! I actually would like to see Fantasy Land itself go down this road, and hope that one day the two specs can merge in such a way that all beneficial properties are kept. :) I'll leave you to go back to your thunk or no thunk discussion. |
I also wish this would happen, and this is sort of proposed in fantasyland/fantasy-land#199. I just don't know what I can do, I'm up for anything. |
Something else entirely: Since this is the PR that introduces association between values and their classes, perhaps this is the best place to discuss another idea related to it. Namely encoding the major spec version into the association. Something like this: { 'static-land/canonical@2': Module, value: x } The Fantasy Land did this by accident. When it introduced a breaking change in the spec (flipping the argument order of I think it would be beneficial to include this from the start in a formalised way. I took the syntax from |
@Avaq that's actually a pretty cool suggestion. Breaking changes shouldn't come lightly, but when they do, it's pretty cool to namespace versions IMO. Watched a cool presentation by Rich Hickey where he's campaigning against breakage in version upgrades, in his own words semantic versioning being a recipe for documenting breakage: https://www.youtube.com/watch?v=oyLBGkS5ICk |
There is a separate issue about versioning #1 (very first thing I thought about, haha). I also hope we just manage to avoid breaking changes entirely, could be hard to do though. |
@rpominov if you have the energy, we can collaborate on that unified repository. I can get involved and I think we can find others as well. Somebody has to start the work, until inertia gets built. For the specification, it's mostly going to be a copy/paste job, plus corrections. From my experience, we'll move slowly, but if we can do a small bit of work each other week, then we'll crack it eventually. I'd also like some (light) types in the repository, in addition to that spec and we can do both TypeScript and Flow. Plus laws specified as code as well. Having to read text in a pseudo-language is harder than it is to read some actual code. But this issue is totally up for debate and unimportant right now. |
Also, a unified repo should probably fork the current Fantasy-Land repository. It's mostly a social issue — contributors are fond of their contributions and forking without actually forking via Git erases valuable history. |
I've been working on fantasyland/fantasy-laws#1, which is somewhat related. |
Sounds interesting, I can't offer much more that's not already in static-land, but together we could compose something better, and present it better. Starting from actually forking fantasy-land sounds like a great idea. I can make a PR to the fork with initial transition. It will look a lot like replacing Fantasy Land with Static Land, but at least I won't have to do such PR to the main repository 😅 |
If the benefits are laid out clearly and a migration path is presented, I expect this change would be widely supported. |
After thinking a bit more, I've decided to open a PR in the main Fantasy Land repository after all, and see what happens. Will do it soon, get ready 😱 |
@rpominov great, but I'd fork the repo into another repository in order for collaboration to happen, otherwise we are limited by comments on the PR and that's going to generate a ton of noise. It's better for that PR to be polished imo, otherwise there will be 2 threads of conversations going on: (1) possible improvements + (2) do we want this or not and it would be good to get the possible improvements out of the way. For example, given that this is a breaking change anyway, I'd like to talk about:
|
Given we have that unified repo, might as well talk there on specifics. Opened my first issue: fantasyland/unified-specification#3 |
Yeah, sorry about the rush. I've got a little too excited for a moment and didn't realize you have many fresh ideas. The |
I've started to prepare the temporary fork here https://github.com/rpominov/fantasy-land |
@alexandru the repository here https://github.com/rpominov/fantasy-land is more or less ready for the PR, you might want to take a look. I've already created a PR about thunks there rpominov/fantasy-land#2 . In any case, I'm going to wait for some time before a PR to main Fantasy Land repo, to give everybody opportunity to propose any changes. |
In the proposition we bind the module that implement operators over target type to every instance of that type. For mitigating the problem of pass around modules when we write generic code why not relay on a solution and convention inspired by Clojure protocols? Instead of binding module to data structure we create a local register of [type: module] associations and call functions against this register. The register dispach to the right module based on some convention. In my implementation I have used '@@type' to tag every data structure and infer the type name. And I have used last param (curryed) as target type. See test implementation, sorry for asbstract names](https://gist.github.com/FbN/65fda3f881420b36e041a0b9f0964aa6) |
Is this gonna happen, or should this proposal be removed from the readme? |
I propose adding the following section to the specification:
Any value may have a reference to a canonical module that works with values of that value's type. The reference should be in the
static-land/canonical
property. For example:In case a value has a reference to a canonical module, that module must produce values with references to itself. In the following example,
list
is an incorrect value, becauseListModule2
does not produce values with references to itself:Note that the
ListModule2
here is correct. Only thelist
value doesn't follow the specification.This will allow for generic code without passing modules. For example:
Any thoughts on the idea itself, or specific details, or wording of the section?
This idea was discussed before in fantasyland/fantasy-land#199
The text was updated successfully, but these errors were encountered: