Skip to content

Const implementation without Fully Qualified Type Representatives #2

@Avaq

Description

@Avaq

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:

  • Const itself is almost Monoidal here, were it not for the other type param (NoConst ++ Const a = Const a, etc)
  • It's the classic layer of indirection: represent your effects instead of embedding them, and run them during interpretation

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

/cc @masaeedu

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions