Reference to a canonical module in values
rpominov opened this issue Β· comments
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:
const ListModule = {
of(x) {
return {'static-land/canonical': ListModule, data: [x]}
},
map(f, v) {
return {'static-land/canonical': ListModule, data: v.data.map(f)}
}
}
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, because ListModule2
does not produce values with references to itself:
const ListModule2 = {
of(x) {
return {data: [x]}
},
map(f, v) {
return {data: v.data.map(f)}
}
}
const list = {'static-land/canonical': ListModule2, data: [1]}
Note that the ListModule2
here is correct. Only the list
value doesn't follow the specification.
This will allow for generic code without passing modules. For example:
function lift2(f, a, b) {
const T = a['static-land/canonical']
return T.ap(T.map(a => b => f(a, b), a), b)
}
lift2((x, y) => x + y, ListModule.of(2), ListModule.of(3)) // ListModule.of(5)
Any thoughts on the idea itself, or specific details, or wording of the section?
This idea was discussed before in fantasyland/fantasy-land#199
π on the proposal. Having modules to pass around is great, however type-class instances are meant to be coherent and static-land
needs a protocol to specify what are THE type class instances for a certain type.
Ah, one small suggestion β I don't necessarily like that static-land/canonical
should be an instance method. For one, this is because having a global, per type dictionary is what separates type classes from OOP classes, so that dictionary really belongs to the "constructor" when dealing with JS classes, because otherwise it signals the wrong semantics β i.e. 2 different values inhabiting that type cannot signal 2 different "canonical" definitions.
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 static-land/canonical
as a static property of Box
instead of as a property on Box
instances. Because you can always get to static-land/canonical
from a Box
instance like boxed.constructor['static-land/canonical']
. The TypeScript compiler will not type-check this, but that's a small price to pay.
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 static-land/canonical
static property will signal the right semantics. But this will require programming style that uses Classes, and I want static-land to support style based on simple objects. For example, we could define a Maybe type like this:
// @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 static-land/canonical
may change over time.
Although maybe adding a property later like this may cause issues with TypeScript, Flow, etc. Maybe to use thunk is a good idea indeed.
But this will require programming style that uses Classes, and I want static-land to support style based on simple objects.
Would this still be possible? Could one define constructor
manually?
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 (x.constructor['static-land/canonical']
and x['static-land/canonical']
) are similar. I think this is more like a messaging issue, e.g. which programming style specification promotes and which discourages.
x.constructor['static-land/canonical']
discourages simple-objects style.x['static-land/canonical']
treats both styles equally well.
BTW, I think we should use __staticLandCanonicalModule__
instead of static-land/canonical
. Two reasons:
- Adding "module" at the end should make code easier to understand.
- Avoiding "-" and "/" should safe us from any potential issues like this one facebook/flow#2482 .
And from the semantics perspective, using thunk would suggest that static-land/canonical may change over time.
In the context of FP, in which we are, that cannot be β a thunk
signals just lazy evaluation, but the call itself has referential transparency. It may indeed return a different reference, or a new reference every time it gets called, but that doesn't matter, being an implementation detail.
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:
Adding "module" at the end should make code easier to understand.
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.
Avoiding "-" and "/" should safe us from any potential issues like this one facebook/flow#2482 .
I think that particular issue is a red herring btw:
- TypeScript supports liberals like that, it's only Flow that has this issue and it's probably a very temporary issue β in a year from now, if Flow keeps evolving, I'm sure that it will get fixed
- adding typing for these fields is kind of useless, because they'll be used dynamically at the call-site anyway
And for instance the issue mentions fantasy-land/of
. Well, it would be nice to have it described with types, but speaking of Fantasy-Land, I don't see the point when you can't describe the monadic bind with types as described in Fantasy-Land β the only language that I worked with and that can describe flatMap
/ chain
as an interface method is Scala and even there it's a pain in the ass, with the interface becoming so parametrized that it ends up being only useful for implementation sharing, but useless for subtype/OOP polymorphism. And if you can't describe an Applicative
or Monad
interface, then the point of having a typed fantasy-land/of
is completely lost.
I have a library at https://github.com/funfix/funfix and I'm rebooting the described type classes to be based on static-land
, with the data types also exposing Fantasy-Land methods and I didn't even bother to describe those methods with types, because it's actually better to have them dynamically compiled, due to the obvious code duplication (e.g. trying to build a method that infers them from the object's own interface, see code sample)
But to get back to the point, static-land/canonical
is much better precisely because it forces people to use literal properties.
The problem with regular properties such as __staticLandCanonicalModule__
is that compilers like Google Closure will minify them, or even remove them completely via tree shaking, unless those properties get specified as literals.
And given that something like static-land/canonical
is a property used dynamically, then it really needs to be there when people do object["static-land/canonical"]
interrogations, with or without an aggressive compiler such as Google Closure being used.
In the context of FP, in which we are, that cannot be
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 empty
property, people always ask why it's a function (#37, fantasyland/fantasy-land#164). Not sure if evaluation strategy argument applies to empty
though, I always thought that it's a function mainly for consistency.
Ease of understanding is irrelevant for this particular field, because this field is not meant for users
I was thinking of a person not familiar with static-land reading source code of a library that implements say Maybe.
Also in my mind "module" as a word is not such a good name
But yes, "module" will only confuse people who're not familiar with this specification π
I agree it is a bad idea to add it.
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!
Yes, from a strictly technical perspective both locations (
x.constructor['static-land/canonical']
andx['static-land/canonical']
) are similar.
Aren't the following two technically similar?
x.constructor['static-land/canonical'].map
x.constructor['fantasy-land/map']
And aren't therefore the following two also similar?
x['static-land/canonical'].map
x.constructor['fantasy-land/map']
This will allow for generic code without passing modules.
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 lift2
has lost the ability to operate using differing instances on the same type, this in my opinion is what sets Static Land apart from Fantasy Land. Now Static Land users would need two kinds of generic functions: one that dispatches to canonical, and one that dispatches to the given type representative. The same can be achieved in Fantasy Land:
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 coercedLift2
, though. I don't think this is a problem with the spec, but rather with communication. In Fantasy Land, all reasoning starts at the value.
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:
I think this is more like a messaging issue, e.g. which programming style specification promotes and which discourages.
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.
Now Static Land users would need two kinds of generic functions
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 lift2(T, f, a, b)
) or they want to use dynamic dispatch (lift2(f, a, b)
). And then just stick with the preferred style. Not sure I understand you correctly though, maybe you've meant something else?
But this generic lift2 has lost the ability to operate using differing instances on the same type, this in my opinion is what sets Static Land apart from Fantasy Land. Now Static Land users would need two kinds of generic functions: one that dispatches to canonical, and one that dispatches to the given type representative.
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 HashMap
implementation is probably screwed.
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 static-land
has and what sets it appart as the right approach going forward, is that Fantasy-Land
as currently defined is anti-types. You can't define a reasonable Monad
interface for Fantasy-Land
in either TypeScript or Flow and that won't change any time soon.
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.
this will make Static Land technically as capable as Fantasy Land while keeping all advantages of Static Land -- @rpominov
I want to go over these advantages:
No name clashes. Since a module is just a collection of functions that don't share any namespace we don't have problems with name clashes.
This actually changes with the introduction of static-land/canonical
. I believe the ['static-land/canonical'].map
property is just as likely to clash as the constructor['fantasy-land/map']
property.
We can implement many modules for one type, therefore we can have more than one instance of the same Algebra for a single type. For example, we can implement two Monoids for numbers: Addition and Multiplication.
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.
We can implement modules that work with built-in types as values (Number, Boolean, Array, etc).
Except that they cannot have the static-land/canonical
property.
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 or they want to use dynamic dispatch -- @rpominov
But when they choose module passing, they are using Static Land pre-#45. When they choose dynamic dispatch, they are essentially using Fantasy Land.
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. -- @alexandru
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 Apply Array
, I thought that's one of the things that spawned Static Land.
You can't define a reasonable
Monad
interface for Fantasy Land in either TypeScript or Flow
I always thought this is simply because of the lack of support for higher kinded types. Eg F<T>
where F
is also a generic. So with the following two assumptions:
- this proposal gives Static Land exactly equal capabilities to Fantasy Land ; and
- TypeScript cannot implement Monad because of the lack of higher kinded types,
I don't see how Static Land brings a solution to this problem.
Which of these assumptions I have are wrong?
You can't define a reasonable Monad interface for Fantasy Land in either TypeScript or Flow
I always thought this is simply because of the lack of support for higher kinded types. Eg F where F is also a generic.
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 this
of course).
TL;DR β the definitions are incompatible with the notion of single dispatch, which is what you get with Fantasy-Land.
The native description of chain
as an interface with single dispatching looks like this:
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 Monad
definition as an OOP interface describing methods (that does single dispatching) is Scala. And in addition to higher-kinded types, for it you need:
- self types
- self-recursive types
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 Chain<A, Self<A> extends Chain<A, Self>>
is not really a type that you can easily use for describing generic functions. But that's not the only problem, the other problem is that monads need the Applicative.of
(or pure
or return
, as named in other languages).
So you actually need this:
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 this
? Other than the use of this
I see no difference between Fantasy Land and Static Land;
//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 this
makes all the difference in the world.
@Avaq to more precisely define the problem β as said above, function parameters are naturally contra-variant, whereas this
has co-variant behavior (because of subtyping), which actually breaks types.
So you can't define a method on an interface that gets more restrictive in implementing types, as it should be with Monad
, because monads don't compose β thus violating the Liskov substitution principle, unless you introduce complicated machinery for F-bounded polymorphism, plus the resulting types end up being too complicated for actual usage.
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 Monad
interface. It's a cool exercise to make.
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:
- Communication: Reasoning is done from the "Module" (or type representative) rather than the "value".
- Cleanliness: In my opinion, attaching a single property to the value or constructor, which refers to a description of the Module is cleaner than having many properties on the
constructor
which combined together form the Module. - No use of
this
: I never realised it's so important, but it makes sense after seeing your explanation. Apparently it makes all the difference to make or break specifying types for this stuff.
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.
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 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 @2
part there contains the major version of the Static Land spec. This would allow for the spec to make breaking changes, and dispatchers/types to implement one or more versions of the spec.
Fantasy Land did this by accident. When it introduced a breaking change in the spec (flipping the argument order of ap
), it also changed the name of the property (from ap
to fantasy-land/ap
), which now allows Fluture to implement both specs, satisfying old dispatchers as well as the ones that kept up with the spec.
I think it would be beneficial to include this from the start in a formalised way. I took the syntax from sanctuary-type-identifiers
, where I introduced something similar to this idea.
@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.
Plus laws specified as code as well.
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 π
Or if anyone else want to make that PR that will be even better, I'll be there for discussions / review in any case. This looks like a plan for a beginning and we could see where this will get us.
It will look a lot like replacing Fantasy Land with Static Land
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:
- function signature convention
- naming
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 fantasyland/unified-specification
repository probably a good place to discuss them, and we can create a temporary fork later as well.
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?