fantasy-land icon indicating copy to clipboard operation
fantasy-land copied to clipboard

Move all functions to namespaced Type Representative

Open briancavalier opened this issue 7 years ago • 50 comments

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:

  1. It standardizes on functions, rather than a mix of functions and methods.
  2. 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)
  3. 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 as Constructor[fl] for cases where where no instance is available.
  4. 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?

briancavalier avatar Oct 30 '16 02:10 briancavalier

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.

rpominov avatar Oct 30 '16 10:10 rpominov

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:

joneshf avatar Oct 30 '16 15:10 joneshf

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?

paldepind avatar Oct 31 '16 11:10 paldepind

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.

rpominov avatar Oct 31 '16 11:10 rpominov

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.

briancavalier avatar Oct 31 '16 12:10 briancavalier

Hi everyone,

Nice to see some good propositions like this one. Here are my two cents on the issues from briancavalier's first post:

  1. 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 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!
  2. 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.
  3. 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

abuseofnotation avatar Nov 01 '16 17:11 abuseofnotation

@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.

evilsoft avatar Nov 01 '16 17:11 evilsoft

@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.

rpominov avatar Nov 01 '16 21:11 rpominov

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.

abuseofnotation avatar Nov 02 '16 16:11 abuseofnotation

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.

rpominov avatar Nov 02 '16 17:11 rpominov

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.

abuseofnotation avatar Nov 03 '16 09:11 abuseofnotation

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

safareli avatar Nov 03 '16 12:11 safareli

🤔

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 .

SimonRichardson avatar Nov 03 '16 15:11 SimonRichardson

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 avatar Nov 04 '16 19:11 syaiful6

@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 avatar Nov 04 '16 19:11 rpominov

@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))
      }
    })

syaiful6 avatar Nov 04 '16 19:11 syaiful6

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).

rpominov avatar Nov 04 '16 20:11 rpominov

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.

syaiful6 avatar Nov 04 '16 20:11 syaiful6

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

abuseofnotation avatar Nov 04 '16 20:11 abuseofnotation

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.

rpominov avatar Nov 04 '16 20:11 rpominov

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.

syaiful6 avatar Nov 04 '16 21:11 syaiful6

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 avatar Nov 04 '16 21:11 rpominov

@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.

syaiful6 avatar Nov 04 '16 21:11 syaiful6

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'].

rpominov avatar Nov 04 '16 21:11 rpominov

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?

syaiful6 avatar Nov 04 '16 21:11 syaiful6

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 avatar Nov 04 '16 22:11 rpominov

@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.

syaiful6 avatar Nov 04 '16 22:11 syaiful6

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 avatar Nov 04 '16 23:11 rpominov

@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

gcanti avatar Nov 05 '16 07:11 gcanti

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.

gcanti avatar Nov 05 '16 09:11 gcanti