Open briancavalier opened 7 years ago
Sounds like a great idea! If we do this right it will effectively make fantasy-land as expressive as static-land while keeping all use-cases of fantasy-land. Here is how we could do this:
"fantasy-land"
that is a link to a corresponding type representative".It's important to not require values to have "fantasy-land"
property, but only say that values may have it. This way we will not loose important features of static-land:
This will also make life easier for implementers and specs maintainers, and unite the community around single spec!
@joneshf You've mentioned couple times in chat room that we should merge specs, very curious on your opinion on this.
I'm down to try whatever, especially if it brings FL and SL closer to merging! I don't really understand how the suggestion makes all those things possible, but I trust you all do :blush:
It's important to not require values to have "fantasy-land" property, but only say that values may have it.
I don't understand this. If values only may have it then one can't rely on values having one.
We're able to have more than one type representative for the same JavaScript type
Can't we get that and still require one of them to be defined at a specific location?
A type may support only static-land style spec or both static-land and fantasy-land styles. It would be the same situation as currently with two specs: some types can support both, some only static-land.
If you support both you get more, like interoperability with Ramda. But if you can't add "fantasy-land"
property to values for some reason, but still can expose type representative somehow (just say in the docs where it's at), you still get a lot.
Can't we get that and still require one of them to be defined at a specific location?
Yeah, no problem with that. One of them can be a default.
If you support both you get more, like interoperability with Ramda. But if you can't add "fantasy-land" property to values for some reason, but still can expose type representative somehow (just say in the docs where it's at), you still get a lot.
I wondered about "may" also. It may be good for the spec (if this idea ends up being adopted) to provide guidance/clarification around this (like you just did :) ), perhaps even strongly recommending the default location for the type representative when it's possible.
Hi everyone,
Nice to see some good propositions like this one. Here are my two cents on the issues from briancavalier's first post:
map
can be defined as a function which is accepts a -> b
as its first argument and a functor value as its this
argument. So that "type representative" thing that you are talking about already exists - its called the prototype. Primitives already have prototypes, but its useful to follow the same convention for them, especially since it may very soon be possible to subclass them![fl]
symbol is a good way to avoid them (even if we don't make everything static). I'd define it as a method which returns a contained object as opposed to a property containing a method dictionary.of
function being in the constructor also sucks. For me the most natural thing will be to have it it in the prototype, as everything else. Some functions take the type as a parameter, as opposed to value? OK then, just pass the prototype.This article is somewhat related to the discussion and it is an interesting read: http://www.haskellforall.com/2012/05/scrap-your-type-classes.html
@boris-marinov: #1 does not fly if people are not using this
. That would force consumers to always go with prototypes. If I am reading that correctly.
for instance I define my ADTs like:
function IO(run) {
const map = fn => ...
...
return { run, map, ap, chain, of}
}
so using this
is not going to fly for all users.
@boris-marinov The point of this proposal is to unite fantasy-land and static-land. Static-land has some advantages over fantasy-land (see pros in the docs and previous comments here). If we do this right fantasy-land will also support cases that only static-land supports without loosing anything. And we will have only one spec. That's why we need type representatives to play the central role instead of using prototypes.
I get that. What I was saying is that an object can be defined by functions that use the this
argument but don't rely on the data they receive to have them, exposed as methods. In that way we will have an truly universal definition:
const array = {
map (f) {
return array.reduce.call(this, (acc, el) => {
acc.push(f(el))
return acc
}, [])
},
reduce(f, acc) {
for (let i = 0; i < this.length; i++) {
acc = f(acc, this[i])
}
return acc
},
chain (f) {
return array.reduce.call(this, (a, b) => a.concat(f(b)), [])
}
}
As you'd imagine, its not hard to convert a function that uses this
to a curried function.
You even get the benefit that you have more information about the function, which you can use to validate the input:
const curryThis = (func) => (...args) => {
if(args.length !== func.length) {
throw new TypeError('Wrong number of arguments')
} else {
return (data) => func.apply(data, args)
}
}
const curryAll = (functions) => Object.keys(functions)
.reduce((curriedFunctions, key) => {
curriedFunctions[key] = curryThis(functions[key])
return curriedFunctions
}, {})
let { map, reduce, chain } = curryAll(array)
let compose = (f, g) => (a) => f(g(a))
let doStuff = compose(chain((a) => [a, a]), map(String))
console.log(doStuff([1, 2, 3])) //['1', '1', '2', '2', '3', '3']
map((a) => a, (a => a)) //TypeError('Wrong number of arguments')
Creating objects from these type definitions is trivial for non-built in values.Built-ins can also be used with the type descriptor, by using underscore-style wrappers:
const wrap = (functions) => {
const proto = Object.keys(functions).reduce((proto, key) => {
proto[key] = function(...args) {
//Just a sample implementation
//Obviously not all functions return the same type of object as they accept
return constructor(functions[key].apply(this.value, args))
}
return proto
}, {})
const constructor = (value) => {
let object = Object.create(proto)
object.value = value
return Object.freeze(object)
}
return constructor
}
let ArrayPlus = wrap(array)
console.log(ArrayPlus([1, 2, 3])
.map((a) => String(a))
.chain((a) => [a, a])
.value)
//['1', '1', '2', '2', '3', '3']
The best part is that this prototype format will instantly be compatible with the new bind operator.
let { map, reduce, chain } = array
[1, 2, 3]::map((a) => String(a))::chain(a) => [a, a])
That is what I had in mind.
Ah, I see. Yea, bind is cool. Although it's only stage 0. We could also write a converter from the current static-land approach to the bind compatible one:
const bindify = T => {
return {
map(f) {
return T.map(f, this)
},
...
}
}
let { map, reduce, chain } = bindify(array)
[1, 2, 3]::map((a) => String(a))::chain((a) => [a, a])
Other way around conversion is also possible of course, so the question is what to choose for default. To me current static-land approach seems like a better default so far, but need to think more about it.
One thing that you should consider is that you cannot have a generic converter from static-land-style type representative to a bind-compatible one, while you can have the reverse.
Also one issue I see in FL is that we can't define typeclass which is parameterized with multiple types, Kleisli Functors for example:
class Monad m => KleisliFunctor m f where
kmap :: (a -> m b) -> f a -> f b
🤔
On Thu, 3 Nov 2016, 12:37 Irakli Safareli, notifications@github.com wrote:
Also one issue I see in FL is that we can't define typeclass which is parameterized with multiple types, Kleisli Functors http://elvishjerricco.github.io/2016/10/12/kleisli-functors.html for example:
class Monad m => KleisliFunctor m f where kmap :: (a -> m b) -> f a -> f b
— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/fantasyland/fantasy-land/issues/199#issuecomment-258130724, or mute the thread https://github.com/notifications/unsubscribe-auth/ACcaGFn8jb3xjmOQf3u6y5QaxNgYMX0zks5q6dVngaJpZM4KkRUs .
static method like this maybe good for simple usage of monad, but when you do more than that, say a monad transformer then good luck for user to give correct dictionary for it.
@syaiful6 Can you give an example? I don't understand monad transformers very well yet, but as far as I understand transformer just takes a monad and gives you another monad with some added effect. So can it be just a function that takes one Type Representative and returns another one?
@rpominov Yes, to define the monad maybe simple, but not all. If you try to define a free monad transformer, maybe to support side effecs for your dsl, then you will require 2 dictionary, one for monad (the effects) and one for the functor (the command).
The writer of monad transformer can just define the free monad, the problem will be more complex when another author use the free monad tranformer for their library, maybe a coroutine. for simplicity, purescript coroutine: https://github.com/purescript-contrib/purescript-coroutines/blob/master/src/Control/Coroutine.purs#L60
here you will require more than 3 all dictionary (2 for functor, 1 monad) - probably more, the order should happen correctly. and the value inside the monad transformer should be correctly the associated with dictionary and value you given on it.
if we just use the current fantasy land, you will just require 1 monad dictionary passed on it, which you can require on top of the module. here my rewrite based on it, where M is the monad used on free monad transformer. if it static, you will have problem which dictionary should be used on map, ap, and pure(of).
const fuseWith = curry(3, (f, fs, gs) => {
return freet.FreeT(() => go(Tuple(fs, gs)))
function go(v) {
let n = ap(
map(
liftA2(f(Tuple)),
parallel(freet.resume(fst(v)))
),
parallel(freet.resume(snd(v)))
)
return chain(next => {
return next.matchWith({
Left: ({ value }) => pure(M, Left(value)),
Right: ({ value }) => pure(M, Right(map(t => freet.FreeT(() => go(t)), value)))
})
}, sequential(n))
}
})
I agree that this seems hard to implement using only Type Representatives, i.e. implement fuseWith
against current static-land spec (although not sure, still digesting it).
But this proposal suggests to not only have Representatives but also to have "fantasy-land" property on each value that contains corresponding Representative. With that in mind I don't understand why this example will be harder compared to current fantasy-land. I mean we still can write a map
that works with any value:
function map(f, v) {
return v["fantasy-land"].map(f, v)
}
And then use such map
and ap
in fuseWith
. As for of/pure
, again nothing different from current fantasy-land, if we don't have a value nor Representative it's impossible to use of
, if we have a value we can do v["fantasy-land"].of(1)
.
Ah, i got it. I think this suggest us to move all function to static one. It's ok i think if we have both.
but it would require the method happen on two locations? on the prototype and the type? what a pain to write the ADT, think ADT sum type. but it look like good to try, since it have some advadtage.
Actually monad transformers are even simpler to implement with type representatives: Type representatives allow you to change the type of a given value without wrapping it in a container and the abundance of containers is one of the biggest downsides of Monad Transformers.
So for example the type of maybeT
in Haskell is
newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }
where the sole purpose of the MaybeT container is to allow for MaybeT m
to have a different monad instance than m
:
instance (Monad m) => Monad (MaybeT m) where
return = lift . return
x >>= f = MaybeT $ do
v <- runMaybeT x
case v of
Nothing -> return Nothing
Just y -> runMaybeT (f y)
Using type representatives, maybeT can be implemented using just (no pun intended :)) one value of type m (Maybe a)
and an augmented type representative like this one (this.outer
. is the type representative of m
):
// (val) => M({value:val})
of (val) { return this.outer.of({value: val, isJust:true }) },
// (val => M({value:val}) , M({value:val})) => M({value:val})
chain (funk, mMaybeVal) {
return this.outer.chain((value) => {
return value.isJust ? funk(value.value) : this.outer.of(value)
}, mMaybeVal)
},
The rest of the code is here
but it would require the method happen on two locations? on the prototype and the type?
No, I guess if we go with this approach methods will live only on Representatives.
But this proposal suggests to not only have Representatives but also to have "fantasy-land" property on each value that contains corresponding Representative.
@rpominov then how the value have "fantasy-land" property without usage of prototype and somehow should be sync on the method live on Representative, right? like Just/Nothing on Maybe?
@boris-marinov i know it simple to define the monad transformer, see my comment above. The problem i see it when use it on another library.
I agree this proposal have some advantages and solve some of current spec i think. I aggree with this propopsal, as long as the value also have "fantasy-land" property on it, and not go static like Static Land spec.
then how the value have "fantasy-land" property without usage of prototype and somehow should be sync on the method live on Representative, right? like Just/Nothing on Maybe?
Sorry, I may have misunderstood the question. prototype
still be used. An implementation of a type compatible with the new approach may look like this:
function Id(x) {
this._x = x
}
const IdRepresentative = {
of(x) {
return new Id(x)
},
map(f, v) {
return new Id(f(v._x))
},
}
Id.prototype['fantasy-land'] = IdRepresentative
// No this stuff any more
// Id.prototype['fantasy-land/map'] = function(f) {...}
@rpominov yes, i think i confused about the term TypeRepresentative, when i read the spec it suggest me it the constructor.
If a type provides a type representative, each member of the type must have a constructor property which is a reference to the type representative.
but then here i think it referred on something else.
Yeah, "Type Representative" in current fantasy-land and in this proposal have slightly different meaning.
It's an object like:
{
'fantasy-land/of'(x) {...},
'fantasy-land/empty'() {...},
'fantasy-land/chainRec'(f, i) {...},
// that's it, only 3 methods
}
That lives on value.constructor
, which also happen to be a reference to the constructor if value was created using classes.
It's an object like:
{
of(x) {...},
empty() {...},
chainRec(f, i) {...},
map(f, v) {...},
ap(f, v) {...},
... // all the rest of the methods also should be here
}
That lives on value['fantasy-land']
.
that make sense now, i think this proposal want the fl methods defined on constructor, which i think just like static land, since it will not available on the instance.
Can we still type it using typescript/flow? is it possible to write it the ADT using these typed js? i dont have strong opinion on it, just many people asks the type definitions for some ADT. what's other advantages of this proposal beside the single location of the method? if it just location, then what's really we gain because of it?
Not sure about types yet, but I think situation may actually improve compared to what we have currently with fantasy-land, or at least not get worse. Writing typings for individual types should be possible. But representing something like type classes in Flow/TypeScript is still tricky, but also may become easier with this approach (I guess flow-static-land may be compatible with this new approach). @gcanti may have a better perspective on this.
what's other advantages of this proposal beside the single location of the method? if it just location, then what's really we gain because of it?
This can basically unite two specs. The key is to not require to have fantasy-land
property, but only say that values may have it. So if you have a Type Representative but values don't reference it in their fantasy-land
property, you still have a spec compatible Type Representative, that is also can be useful. I've explained this in previous comments: https://github.com/fantasyland/fantasy-land/issues/199#issuecomment-257142130 https://github.com/fantasyland/fantasy-land/issues/199#issuecomment-257270959
@rpominov sorry, i am with @paldepind comment above and doesn't understand it even after reading your comment. but, just go with this proposal. it would make sense to see the actual PR i think.
I think we should just make a distinction between spec compatible Type Representatives and spec compatible values.
Spec compatible Type Representative is just a dictionary with certain functions, for which certain laws stand, for example F.map(x => f(g(x)), a) ≡ F.map(f, F.map(g, a))
.
And spec compatible value is a value that has a 'fantasy-land'
property that points to a Type Representative that can work with values of same type. So we won't have to say that spec compatible value may have the property. We will say that it must have it in order to be a fantasy-land compatible value.
Having this two separate artefacts, people may choose to totally ignore values part and write their generic code against Type Representatives. Or they may use values part as well. Choosing one way or another has certain trade-offs described above.
One important detail: TypeRep.of(1)['fantasy-land'] !== TypeRep
should be allowed. Representatives should be allowed to produce spec incompatible values, or values that have reference to some other Representative.
Update:
TypeRep.of(1)['fantasy-land'] !== TypeRep
should be allowed.
But probably not in the case when we get Representative from a value. In other words this should not be allowed v['fantasy-land'].of(1)['fantasy-land'] !== v['fantasy-land']
. We can describe this exception in values part.
Udate 2:
See example https://gist.github.com/rpominov/6b4462137aff8de92dbd078da6a3564c
@rpominov I'd be glad to help with the Flow typings, but I don't understand how all this should work. Perhaps a concrete implementation example, let's say Maybe
, would be helpful
Another observation: even with the proposed changes I don't see how to encode in fantasy-land the following two monoids
(number, *, 1)
(number, +, 0)
without modifying the Number
constructor or the Number
prototype (or wrap numbers in a class). Moreover I fail to see how to encode both of them at the same time, so I must make an arbitrary choice. static-land seems strictly more powerful in this regard.
The idea, I think, is to have a the type definition as a separate argument, so for example an implementation of fold would be:
let fold = (MonoidA) => (listA) =>
listA.reduce((a1, a2) => MonoidA.concat(a1, a2), MonoidA.id())
fold(sumMonoid)([1, 2, 3, 4])) //10
fold(multiplicationMonoid)([1, 2, 3, 4]) //24
I will leave another link to this article, in case you missed it, because I think it talks about the same thing: http://www.haskellforall.com/2012/05/scrap-your-type-classes.html
Here are the monoid instances:
const sumMonoid = {
id() {
return 0
}
,concat(a1, a2) {
return a1 + a2
}
}
const multiplicationMonoid = {
id() {
return 1
}
,concat(a1, a2) {
return a1 * a2
}
}
@boris-marinov that is how static-land actually works. But what about fantasy-land? If we admit explicit dictionary passing, then we can just drop fantasy-land for static-land
@gcanti The idea is to unite the two, have a single spec that allows both approaches. See https://github.com/fantasyland/fantasy-land/issues/199#issuecomment-258568380 and also https://github.com/fantasyland/fantasy-land/issues/199#issuecomment-257142130 https://github.com/fantasyland/fantasy-land/issues/199#issuecomment-257270959
Created some examples here https://gist.github.com/rpominov/6b4462137aff8de92dbd078da6a3564c
@rpominov That would be great, though from the discussion I'm not sure how. Something like this?
// @flow
const maybeFunctor = {
map<A, B>(f: (a: A) => B, fa: Maybe<A>): Maybe<B> {
return fa instanceof Nothing ? fa : new Just(f(fa.value))
}
}
class Nothing {
static fantasyLand = maybeFunctor
map(f) {
return maybeFunctor.map(f, this)
}
fantasyLand() {
return maybeFunctor
}
}
class Just<A> {
value: A;
static fantasyLand = maybeFunctor
constructor(value: A) {
this.value = value
}
map<B>(f: (a: A) => B) {
return maybeFunctor.map(f, this)
}
fantasyLand() {
return maybeFunctor
}
}
type Maybe<A> = Just<A> | Nothing;
const double = n => 2 * n
const x: Maybe<number> = new Just(1)
// 4 ways to map
x.map(double)
maybeFunctor.map(double, x)
x.fantasyLand().map(double, x)
x.constructor.fantasyLand.map(double, x)
EDIT: ah sorry, I didn't see your last comment before submitting
Yeah, something like this. Pity that Flow doesn't allow 'fantasy-land'
as a name for the property:
static 'fantasy-land' = maybeFunctor
^ literal properties not yet supported
Same issue with current fantasy-land https://github.com/facebook/flow/issues/2482
I'd like to provide a different point of view, hopefully alleviating some of the confusion in this thread, and introducing a new idea:
Static Land can have multiple Type Representatives for the same value, which is great; for example we can do Array
and ZipArray
types which behave differently. The model is that a Representative knows about its values, but values don't know about their Representatives.
The latter is a problem. It forces us to pass around the Representative along with the value every time we call a function which needs to know the Type (and its implementation). Fantasy-Land solves this problem by associating the Representative with the value itself. In v1, values pointed to their Representative through the __proto__
property, convenient because it's assigned automatically by JavaScript. In v2 we changed it to the constructor
property, convenient because it too is assigned automatically by JavaScript.
* In cases where the type is constructed using JavaScripts inheritance model.
The inconvenience in both cases, however, is that we're sharing a name-space with other stuff. Whether it be the method namespace or static namespace. In order to combat this issue we've prefixed every property which is part of the Type Representative, so it can live in harmony with whatever else lives there. Now we're looking forward to change the location of the type-representative once again, and finally to a destination where the properties don't have to share. What we end up with are Representatives which are completely compatible with Static Land Representatives. It's like we're using Static Land, but giving values some knowledge about their Representatives by association through the fantasy-land
property, or wherever we decide to put it.
A more interesting question, to me, is whether we can encode the multiple Type Representatives thing in a useful way. We're stuck in associating a single Type with every value, because that's how we started. But maybe we can do something useful if we associate all types of a value with it; say: Array.prototype['fantasy-land/types'] = [Array, ZipArray]
. Possibly allowing for a new breed of dispatching logic.
Edit: I should clarify that the above approach probably won't gain us anything, but it does demonstrate an idea which flows forth out of the point of view I'm trying to get across, where Fantasy Land merely specifies a way to associate values with their Type(s).
@gcanti, in your example Isn't it possible to add the type param also to Nothing
, so Nothing<string> !== Nothing<Number>
Sorry for being off-topic.
There're some discussions going on in static-land's repository, that may be related to this proposal. Would very appreciate any input there!
https://github.com/rpominov/static-land/issues/32 https://github.com/rpominov/static-land/issues/34
To move this forward I've created a separate repo where we can discuss specifics of the new spec in individual issues. Hopefully nobody minds. Otherwise this issue will became too big.
https://github.com/fantasyland/unified-specification
I'll open couple issues there soon.
I'd rather you forked fantasy-land and then done a PR imo.
I would love to, but actually I'm not sure I'm good for the task. I think this'll require basically a rewrite from a whole different perspective. And having wrote static-land I'm not sure I like the result. My english is mediocre, and education in some areas related to the specification also not great. So not sure I should do this, but I want to help in other ways.
Also it feels like we need more discussion on the specifics before any PR. And it won't be manageable to do all of them in this issue, or inside a PR. So I thought having a temporal repository to manage this process is a good idea.
Fair play, continue ;-) nothing to see here.
On Sun, 27 Nov 2016, 17:38 Roman Pominov, notifications@github.com wrote:
I would love to, but actually I'm not sure I'm good for the task. I think this'll require basically a rewrite from the whole different perspective. And having wrote static-land I'm not sure I like the result. My english is mediocre, and education in all areas related to the specification also not great. So not sure I should do this, but I want to help in other ways.
Also it feels like we need more discussion on the specifics before any PR. And it won't be manageable to do all of them in this issue, or inside a PR. So I thought having a temporal repository for this is a good idea.
— You are receiving this because you commented.
Reply to this email directly, view it on GitHub https://github.com/fantasyland/fantasy-land/issues/199#issuecomment-263135517, or mute the thread https://github.com/notifications/unsubscribe-auth/ACcaGM0hHVLUapdTzVOOTM-rU3YZshTbks5rCcAogaJpZM4KkRUs .
Hi all. Could you please vote on this?
In theory, if I'd create a PR for this, would you prefer it to has minimal amount of changes, or would bring most of static-land with itself?
🎉 for as minimal changes as possible, ❤️ for the whole package.
Note that I've worked a lot on the structure and terminology of static-land, and in my opinion it would make sense to use what I've ended up with. If we try to preserve that of current Fantasy Land document the result might be confusing and harder to understand.
Also note that if you vote I will not automatically expect of you to support the PR, just want to know what you think has a higher chance of moving forward.
And I probably won't open a PR right away, there will be a temporary fork, which we'll be able to polish before an attempt on final merge. I just want to understand what is better direction in your opinion.
I've started to prepare repository for a PR here https://github.com/rpominov/fantasy-land. Feel free to open PRs in it for any changes you want to be made before the big PR to the main repository. Ideally, in the final PR, we should discuss only whether we want it to be merged, and decide on all details before.
Further to Roman's point, please add yourself as a watcher of rpominov/fantasy-land if you're interested in following the preliminary work and making suggestions. :)
@rpominov Unless I'm mistaken it's impossible to create issues in rpominov/fantasy-land. Is that intentional?
@paldepind Sorry, missed your comment. I've turned on issues now, it's probably off by default for forks.
I'll probably won't be doing any changes to the repository unless something comes up. But I'm going to wait for any PRs or issues for some time before opening the big PR.
Going to open the PR in couple days. I figured opening the PR is not a big deal, we'll still be able to discuss and make changes in the fork, and PR will be updated automatically.
Recently, I started updating two libs (creed and most) to support FL 2.0, and added Static Land support to most. In the process, I read this thread that led to standardizing on Type Representatives for static functions like
of
andempty
.All of that led me to the thought of leveraging Type Representatives more fully. I wanted to put the idea out for discussion to see if it seems interesting:
Rather than many namespaced methods, e.g.
x[fl.map](f)
, FL could move to functions on a single namespaced Type Representative, e.g.x[fl].map(f, x)
, wherex[fl]
is the location ofx
's Type Representative, andfl
is the only required FL string/Symbol.This is obviously a substantial change, but I think it would have several benefits:
.constructor
as the location of the Type Representative. Although, it may also be worth considering whether it's useful to add the Type Representative asConstructor[fl]
for cases where where no instance is available.I don’t think this has many downsides compared to the current FL 2.0 approach. For example, ergonomics: the prefixed methods required by FL 2.0 are, imho, already fairly unergonomic for app devs. Library devs will endure them in order to get the benefits of being FL compliant. Moving to functions on a single namespaced type representative seems like it retains the same level of ergonomics.
Any FL delegation library such as fantasy-sorcery or Ramda would need to change its dispatching, which would be a breaking change, but the dispatching is no more complex.
What do you think of the idea of having all functions on a single namespaced Type Representative?