sanctuary-js / sanctuary-constant

:rainbow: Fantasy Land -compliant Constant type
2 stars 1 forks source link

Const implementation without Fully Qualified Type Representatives #2

Open Avaq opened 3 years ago

Avaq commented 3 years ago

During a recent conversation between @davidchambers and I, we came up with a way to define an Applicative Const Functor that can be used to implement Lenses, without needing a Monoid up front. The conversation is starting to escape my memory, so I'll just put the idea down here lest we forget.

The Problem

As I understand, the need for a Monoid on Const currently blocks progress on this library. In particular, there currently is no way to implement Fantasy Land's Applicative for Const in the way Haskell does:

data Const a b = Const {getConst :: a}

instance Monoid m => Applicative (Const m) where
    pure _ = Const mempty
    (<*>) = coerce (mappend :: m -> m -> m)

Here we see Applicative implemented for Const m by restricting to ms that have Monoid, and just putting mempty as the actual context-value of the Const. Haskell then relies on the type system to make it so when getConst is resolved, it gets a value of the correct specific Monoid.

In JavaScript (and specifically, Fantasy Land as it is right now), we don't have a way to carry type information along with our Const instances so that when getConst is used, we can put a value of the correct Monoid there:

Const['fantasy-land/of'] = function(_){
  return new Const(/* What do I put here?! */)
}

Solutions


An alternative that I came up with is to have a binary type implementation for Const, where reliance on the type system is removed, and the fallback value is provided by the user when evaluating the Const:

import {tagged} from 'daggy';

const Constant = tagged('Const', {
  Const: ['context'],
  NoConst: [],
});

// We can implement pure by simply returning NoConst:
Constant['fantasy-land/of'] = function(_){
  return Constant.NoConst;
};

Constant.prototype['fantasy-land/map'] = function(_){
  return this;
};

Constant.prototype['fantasy-land/ap'] = function(other){
  return other.cata({
    NoConst: () => this,
    Const: otherContext => this.cata({
      NoConst: () => other,
      Const: context => Constant.Const (otherContext['fantasy-land/concat'](context))
    })
  });
};

// The provision of a Monoid is moved all the way to the evaluation step:
const getConst = monoidRepr => c => c.cata({
  NoConst: () => monoidRepr['fantasy-land/empty'](),
  Const: context => context
});

A few observations:


What do you think? Does this approach have any benefits over the one taken in #1 ?

/cc @masaeedu

Avaq commented 3 years ago

I think ap could also be implemented on top of getConst, like:

Constant.prototype['fantasy-land/ap'] = function(other){
  return other.cata({
    NoConst: () => this,
    Const: otherContext => Constant.Const (
      otherContext['fantasy-land/concat'] (getConst (otherContext.constructor) (this))
    )
  });
};
Avaq commented 3 years ago

We can even make the Applicative implementation conditional, like done in other types to get better type introspection:

//...
function Const(x){
  const cnst = Object.create (Const$prototype);
  if(Monoid.test(x)){
    cnst['fantasy-land/ap'] = Const$prototype$ap;
  }
}
//...

Applicative.test (Const ("_")) // true
Applicative.test (Const (42)) // false
Applicative.test (NoConst) // true
Avaq commented 3 years ago

Ah. I just remembered another idea David and I had: Since Lenses don't actually use the Applicative instance on Const, we could export two Const types from this library: One that just has Functor, and doesn't need a Monoid typeRep when evaluated. This one could be used for lens implementations. And the other being the Applicative Functor variant proposed above.

masaeedu commented 3 years ago

Hello @Avaq. I think this is a pretty good idea. It's worth noting that any Apply is susceptible to this trick. So in Haskell, you can write:

data AddPure f a = Pure a | Impure (f a)

instance Apply f => Applicative (AddPure f)
  where
  pure = Pure

  Pure ab <*> Pure a = Pure $ ab a
  Pure ab <*> Impure fa = Impure $ ab <$> fa
  Impure fab <*> Pure a = Impure $ (\ab -> ab a) <$> fab
  Impure fab <*> Impure fa = Impure $ fab <.> fa

interpret :: Apply f => (forall x. x -> f x) -> AddPure f a -> f a
interpret pure (Pure a) = pure a
interpret _ (Impure fa) = fa

All this to say the approach you've outlined definitely works and gives a lawful Applicative. It's possible that a general purpose utility for "deferring" the pure implementation of Applys would be useful, although I can't immediately see a use for it beyond Const.

Avaq commented 3 years ago

Thanks for your thoughts @masaeedu!

It's possible that a general purpose utility for "deferring" the pure implementation of Applys would be useful, although I can't immediately see a use for it beyond Const.

My inclination would be to go with the const-specific approach initially, and extract out if we find ourselves duplicating the approach.