Skip to content

Instantly share code, notes, and snippets.

@miwillhite
Last active December 3, 2020 00:50
Show Gist options
  • Save miwillhite/73fee45d512681e18eba782ec9704f66 to your computer and use it in GitHub Desktop.
Save miwillhite/73fee45d512681e18eba782ec9704f66 to your computer and use it in GitHub Desktop.
A naïve Validation ADT implementation in JS vs ReasonML vs PureScript
/*
JavaScript
There are 2 files.
1) The ADT definition.
Here we define the data constructors, their properties and the
implemenations (or "instances") of the various methods, in this
case "concat".
2) The sanctuary-def type definition.
This is needed to make Sanctuary functions (e.g. `concat`) "aware"
of our ADT so that it is able to type check it at runtime.
We need to tell Sanctuary how to identify the object being tested
and how to extract the values out of it so that they can be checked
as well.
Advantages:
1) It's vanilla JS, once you get used to the dance, it isn't complicated
2) Writing `concat` on the prototype allows us to act like we have typeclasses
this means that we can use any implementation of `concat(a, b)` and we know
it'll work the same. Also we can switch out ADTs (Array vs List vs LazyList)
and we won't have to touch the functions operating on that ADT.
Disadvantages:
1) Verbosity of syntax to express simple ideas
2) A lot of boilerplate to get our ADT into Sanctuary's environment
3) Assigning the fn to two places on the prototype in order to support
two versions of Fantasy Land spec (v2 requires prefixed names: 'fantasy-land/concat')
*/
// types/validation/index.js
//
import { taggedSum } from 'daggy';
import $ from 'sanctuary-def';
import {
concat as concatFL,
} from 'fantasy-land';
import {
concat,
} from 'vc-sanctuary';
import {
typeIdent,
} from './type';
// Validation a b = Failure a b | Success b
const Validation = taggedSum(typeIdent, {
Failure: ['a', 'b'],
Success: ['b'],
});
export const { Failure, Success } = Validation;
// :: Validation a => a -> a -> a
Validation.prototype.concat =
Validation.prototype[concatFL] =
function Validation$concat (r) {
return this.cata({
Failure: (a, b) => r.cata({
Failure: (r$a, r$b) => Failure(concat(a, r$a), concat(b, r$b)),
Success: r$b => Success(concat(b, r$b)),
}),
Success: b => r.cata({
Failure: (r$a, r$b) => Failure(r$a, concat(b, r$b)),
Success: r$b => Success(concat(b, r$b)),
}),
});
};
export default Validation;
// types/validation/type.js
//
import $ from 'sanctuary-def';
import { cata } from '../utils';
import type from 'sanctuary-type-identifiers';
// :: String
export const typeIdent = 'vc/Validation';
// :: (Type, Type) -> Type
export const $Validation = $.BinaryType(
typeIdent,
'',
x => type(x) === typeIdent,
cata({
Failure: (a, b) => [],
Success: b => [],
}),
cata({
Failure: (a, b) => [b],
Success: b => [b],
}),
);
// :: Type
export const ValidationType =
$Validation($.Unknown, $.Unknown);
export const env = [
ValidationType,
];
-- PureScript
--
-- Note: PureScript's `concat` is called `append`.
--
-- Advantages:
--
-- 1) The ADT definition syntax is terse, making it expressive and crystal clear
-- 2) We have typeclasses! So much like the prototype implementation, we define an
-- "instance" of Semigroup (which in this case means we define `append`).
-- Then the Prelude (default functions, this would be like Ramda or Sanctuary)
-- gives us an `append` function that can be used with anything that implements Semigroup.
-- When we call `append(v1, v2)` the instance defined below will be used.
-- 2) Pattern matching is great here too!
-- 3) Polymorphism all the way down, notice the `a` and `b`...we don't care what type they are,
-- just that they also implement Semigroup. So they could be Strings, Arrays, Validations, etc
--
-- Disadvantages:
--
-- 1) It isn't JavaScript, so we require more tooling.
module Main where
import Prelude
data Validation a b
= Failure a
| Success b
instance semigroupValidation :: (Semigroup a, Semigroup b) => Semigroup (Validation a b) where
append (Success a) (Success b) = Success $ append a b
append (Failure a) (Failure b) = Failure $ append a b
append (Failure a) _ = Failure a
append _ (Failure a) = Failure a
/*
ReasonML
Advantages:
1) It's a nice syntax for defining types, we even get "type variables" or "variants"!
2) Pattern matching is robust and easy to use
Disadvantages:
1) If we want to concatenate two validations we have to use this module: Validation.concat(v1,v2)
This means that our seemingly declarative code operating on the ADT is explicitly tied to the
data type. This means that if we want to switch ADTs then we have to also switch out the concat
fn that operates on them.
2) Notice how we handle two Success values. We concat the `a` and `b` inside of them. Again we are
lacking polymorphism here. The ++ only operates on string values. If we wanted to make that piece
polymorphic...we just can't. That breaks Validation in our application where Validation can wrap
Field *and* Form types.
3) It isn't JavaScript, so we require more tooling.
*/
type validation('a, 'b) = Failure('a) | Success('b);
let concat = (left, right) =>
switch (left, right) {
| (Success(a), Success(b)) => Success(a ++ b)
| (Failure(a), Failure(b)) => Failure(a ++ b)
| (Failure(a), _) => Failure(a)
| (_, Failure(a)) => Failure(a)
};
@masaeedu
Copy link

masaeedu commented Mar 21, 2018

For comparison, a more terse (but less production ready) approach in JS:

const Vld = ({ concat }) => ({
  concat: a => b => {
    const s = a.s && b.s
    const v =
      (a.s && b.s) || (!a.s && !b.s) ? concat(a.v)(b.v) : !a.s ? a.v : b.v

    return { s, v }
  }
})

const Str = {
  concat: a => b => a + b
}

// quick impl of arr to do permutations
const Arr = (() => {
  const map = f => as => as.map(f)
  const pure = x => [x]
  const ap = af => av => chain(f => chain(x => pure(f(x)))(av))(af)
  const chain = f => as => as.reduce((p, c) => [...p, ...f(c)], [])

  return { map, pure, ap, chain }
})()

const StrVld = Vld(Str)

const rainbow = { s: true, v: 'Everything was fine!' }
const explosion = { s: false, v: 'Explosion!' }

// all permutations of rainbows and explosions
const items = [rainbow, explosion]
const permute = as => Arr.ap(Arr.map(StrVld.concat)(as))(as)
console.log(permute(items))

/*
[
  {
    "s": true,
    "v": "Everything was fine!Everything was fine!"
  },
  {
    "s": false,
    "v": "Explosion!"
  },
  {
    "s": false,
    "v": "Explosion!"
  },
  {
    "s": false,
    "v": "Explosion!Explosion!"
  }
]
*/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment