fantasy-land
fantasy-land copied to clipboard
Move all functions to namespaced Type Representative
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
and empty
.
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)
, where x[fl]
is the location of x
's Type Representative, and fl
is the only required FL string/Symbol.
This is obviously a substantial change, but I think it would have several benefits:
- It standardizes on functions, rather than a mix of functions and methods.
- It reduces the number of namespaced properties on instances to exactly 1, allowing functions on the Type Representative to be non-namespaced (though they could remain namespaced if that's desirable)
- It removes the need to use
.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. - While it doesn't completely unify FL and Static Land, it does come close:
- this new, richer Type Representative is effectively a Static Land dictionary, and
- provides a standard location for Static Land dictionaries. When an instance is available, it reduces the need to explicitly pass Static Land dictionaries around.
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?
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:
- Most of the spec would look like static-land (will focus on describing type representatives, all laws will be expressed in terms of type representatives, etc.). This will eliminate need of static-land to exist, and we can borrow most of it from static-land.
- At the end it would have a statement like "values may have a special property
"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:
- We don't have to do wrapping which allows for much simpler code sometimes (example, also Traversable derivations).
- We're able to have more than one type representative for the same JavaScript type (example).
- We're able to implement spec support as a separate package, if an original package's authors don't want to add compatibility for some reason.
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:
- Maybe we shouldn't be so obsessed about the Function/Method dichotomy - in JS methods are functions and vice versa. So currently
map
can be defined as a function which is acceptsa -> b
as its first argument and a functor value as itsthis
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! - Namespaced methods suck and the
[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. - That thing about the
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)), [])
}
}
Using with point-free style
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')
Using with OOP
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']
Using with the ES7 Bind operator
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, [email protected] 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.
In current spec
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.
In this proposal
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.
If you're reading through discussion, please read this comment. This one is important.
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.