New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

`class`-less protocol implementation #27

Closed
azz opened this Issue Oct 14, 2017 · 9 comments

Comments

Projects
None yet
7 participants
@azz
Copy link

azz commented Oct 14, 2017

I've just been experimenting with the proposal a bit, when implementing Applicative and Maybe's instance of it, I ended up with:

// JS
protocol Applicative extends Functor {
  static pure;
  ap;
}

class Maybe implements Applicative {
  static pure(value) {
    return Maybe.just(value)
  }
  ap(mb) {
    if (this._value === nothing) return this;
    return mb[Functor.fmap](this._value);
  }
}

Note that I gravitated to putting state inside this, i.e. the Maybe, when the state actually belongs inside the Just, as it should be an algebraic data-type.

It would be great if we could also do this without classes, because they restrict the ability to do currying, don't work so great with pattern matching, and (in my opinion) encourage putting state in all the wrong places.

Here is an alternative:

protocol Applicative extends Functor {
  pure;
  ap;
}

const Just = value => ({ type: "Just", value });
const Nothing = { type: "Nothing" };

// NB: intentional use of object without `prototype` property
const Maybe = Protocol.implement({
  [Applicative.pure]: Just,

  [Applicative.ap]: ma => mb =>
    ma.type === "Just" ? Maybe[Functor.fmap](ma.value)(mb)
                       : Nothing
}, Applicative);

Which aligns much more closely to Haskell's instance:

instance Applicative Maybe where
    pure = Just

    Just f  <*> m       = fmap f m
    Nothing <*> _m      = Nothing

We could even pattern match:

const Maybe = Protocol.implement({
  [Applicative.pure]: Just,

  [Applicative.ap]: ma => mb => match (ma) {
    { type: "Just", value }: Maybe[Functor.fmap](value)(mb)
    { type: "Nothing" }:     Nothing
  }
}, Applicative);

And as a possible syntax extension:

const Maybe = implements Applicative {
  [Applicative.pure]: Just,

  [Applicative.ap]: ma => mb => match (ma) {
    { type: "Just", value }: Maybe[Functor.fmap](value)(mb)
    { type: "Nothing" }:     Nothing
  }
};

Which, to me, looks 🏆

@keithamus

This comment has been minimized.

Copy link

keithamus commented Oct 15, 2017

How about?

// JS
protocol Applicative extends Functor {
  static pure
  ap
}

class Just extends Maybe implements Applicative {
  [Applicative.ap](mb) { return mb[Functor.fmap](this._value) }
}
class Nothing extends Maybe implements Applicative {
  [Applicative.ap](mb) { return this }
}
class Maybe implements Applicative {
  static [Applicative.pure](value) { return new Just(value) }
}
@azz

This comment has been minimized.

Copy link
Author

azz commented Oct 15, 2017

That would be the OO way of doing it, yes. But it means you need more classes and more methods. It also seems a bit strange that Just extends Maybe, while it fits the purpose here it seems like the wrong relationship. Additionally Maybe is now somewhat abstract and could throw if you try to construct it. In which case why have a class in the first place?

@bmeck

This comment has been minimized.

Copy link

bmeck commented Oct 25, 2017

Can you clarify

Note that I gravitated to putting state inside this, i.e. the Maybe, when the state actually belongs inside the Just, as it should be an algebraic data-type.

Just is a tagged form of a Maybe. In other languages like Rust both None and Some are Options. You cannot unbox the Just/Some to be on its own.

@michaelficarra

This comment has been minimized.

Copy link
Owner

michaelficarra commented Oct 25, 2017

@azz Thanks for the suggestion, but I think the current design fits better with the rest of JavaScript as a language. The idiomatic way to do this would be as @keithamus suggests. There's actually an example of a Maybe type implemented in this way already in the examples.

@aluanhaddad

This comment has been minimized.

Copy link

aluanhaddad commented Oct 25, 2017

@michaelficarra I don't see why protocols should be implementable by classes only. It seems completely reasonable to want to implement a protocol with an object.

@gabejohnson

This comment has been minimized.

Copy link

gabejohnson commented Oct 25, 2017

@aluanhaddad this is something that can be built on top of Protocol.implement

const implementForObject = (o, p) => new Protocol.implement(function () {
  this.constructor.prototype = o;
}, p);

The usage is the same as in your example @azz

const Maybe = implementForObject({
  [Applicative.pure]: Just,

  [Applicative.ap]: ma => mb =>
    ma.type === "Just" ? Maybe[Functor.fmap](ma.value)(mb)
                       : Nothing
}, Applicative);
@michaelficarra

This comment has been minimized.

Copy link
Owner

michaelficarra commented Oct 26, 2017

@gabejohnson Nice idea. This is how I would do it:

const implementForObject = (o, ...protocols) => {
  class C{}
  C.prototype = Object.create(o);
  return new Protocol.implement(C, ...protocols);
};
@caub

This comment has been minimized.

Copy link

caub commented May 1, 2018

Forgive me if this is not totally related to this issue (else I'll open a new one)
But what I'd hope this proposal can also solve, is to simply enforce a plain object structure.

For example, imagine I'm dealing with simple {x: 1.2, y: 0.9} position objects, I'd like to be able to do:

protocol Coords {
  x;
  y;
}

// the current 'unsafe' way, e.g. if I do a typo on a prop `{x: 1.2, u: 0.9}`
// const coords = {x: 1.2, y: 0.9}; 

// this should work with this proposal? but it's verbose
const coords = Protocol.implement({x: 1.2, y: 0.9}, Coords); 

// const coords implements Coords = {x: 1.2, y: 0.9}; // would be cooler
// const coords : Coords = {x: 1.2, y: 0.9}; // would be ideal
// const coords : Coords = {x: 1.2, u: 0.9}; // would throw, since `u` doesn't respect the protocol 
@michaelficarra

This comment has been minimized.

Copy link
Owner

michaelficarra commented May 7, 2018

@caub While you could use this feature to do what you want, it would probably be more appropriate to use the proposed pattern matching feature instead.

With protocols:

protocol Coords {
  static "x";
  static "y";
}

const coords = {x: 1.2, y: 0.9};

Protocol.implement(coords, Coords); 
console.log(coords implements Coords);

With pattern matching:

function looksLikeCoords(coords) {
  return match (coords) {
    {x, y} => true,
    _ => false,
  };
}

const coords = {x: 1.2, y: 0.9};

if (!looksLikeCoords(coords)) throw new Error("message");
console.log(looksLikeCoords(coords));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment