Last active
December 3, 2020 00:50
-
-
Save miwillhite/73fee45d512681e18eba782ec9704f66 to your computer and use it in GitHub Desktop.
A naïve Validation ADT implementation in JS vs ReasonML vs PureScript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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, | |
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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) | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
For comparison, a more terse (but less production ready) approach in JS: