1.0.0-rc27 • Published 4 years ago

stags v1.0.0-rc27

Weekly downloads
36
License
MIT
Repository
-
Last release
4 years ago

stags

pipeline status

coverage report

A simple library for complex logic.

Quick Start

npm install stags@next
import { either } from "stags";

// Create specific types for specific scenarios.
const Loaded = 
    either("Loaded")

const loaded = 
    Loaded.Y("Hello World")

const loading = 
    Loaded.N(55)

const render = 
    Loaded.bifold(
        x => `Loading: ${x}%`
        , x => `Loaded: ${x}`
    )

const transform = 
    Loaded.map(
        x => x.toUpperCase()
    )



render( transform( loaded ) )
//=> 'Loaded: HELLO WORLD'

render( transform( loading ) )
//=> 'Loading: 55%'

transform( render( loading ) )
//=> 'LOADING: 55%'

What is it

Freedom from booleans.

Scenario: You've solving a moderately difficult problem, and there's a degree of data modelling involved. You've got several booleans for tracking loading states, save states, modified states, selected states and on and on.

Oh! and you're tracking all those states for every item in a list separately.

Depending on a specific combination of these boolean flags you need to render differently, talk to the server differently, persist state differently.

It very quicky becomes a mess. We reach for complex tools to help us manage the mess. But instead all we needed was to make it impossible to not traverse every single state.

First step. Create a stags for every state we want to track.

import { either } from 'stags'

const [Selected, Loaded, Modified, Saved] =
    ['Selected', 'Loaded', 'Modified', 'Saved'].map(either)

Every one of your types has a Y and N constructor that will return a simple object around any value you want to wrap.

Y is a shorthand for Yes and N is a shorthand for No.

Selected.Y(data)
//=> { type: 'Selected', case: 'Y', value: data }

Selected.N(data)
//=> { type: 'Selected', case: 'N', value: data }

Our constructors just tag our data with a label that allows us to build logic on top of it. Normally this label would be stored separately to the data.

Something like:

var selected = false
var data = ...

Which is fine at first, until it looks like this:

var selected = true
var loaded = true
var modified = true
var saved = false
var data = x

We'd model that in stags like so:

import { either } from 'stags'

const [Selected, Loaded, Modified, Saved] =
    ['Selected', 'Loaded', 'Modified', 'Saved'].map(either)

const data =
    Selected.Y( Loaded.Y( Modified.Y( Saved.N( x ) ) ) )

The above would create a giant nested object which would be awkward to traverse manually. But that's a good thing. We should never manually peek into the returned object's inner state in application code.

Instead we can use any number of useful helpers. Like map, fold, bimap chain or bifold.

const f =
    Selected.map(
        Loaded.map(
            Modified.map(
                Saved.bimap(
                    x => 'unsaved'
                    ,x => 'saved'
                )
            )
        )
    )

f( data )
//=> Selected.Y( Loaded.Y( Modified.Y( Saved.N('unsaved') ) ) )

If we wanted to traverse and extract at the same time, we could use fold instead of map. But then we have to handle all the N states along the way too.

const f =
    Selected.bifold(
        () => 'unselected',
        Loaded.bifold(
            () => 'loading',
            Modified.map(
                () => 'unmodified',
                Saved.bimap(
                    x => 'unsaved'
                    ,x => 'saved'
                )
            )
        )
    )

f( data )
//=> 'unsaved'

If we pass the wrong data structure into our composition, we will get a specific, helpful error message explaining what your type looked like and what that particular method was expecting.

Your stack trace is going to be particularly legible because stags internally avoids point free composition.

Every error that stags yields, is itself a transformation of a stags. All the error types are documented in the Errors section

Specification

stags differentiates itself by documenting the internal structure used for types and instances of types. This allows you to create your own constructors/transformers in userland. You can store the exact output of a stags constructor in a redux-store, localStorage or even a json column in postgres.

stags does not care where your data came from, just that it adheres to a particular structure.

Ecosystem

Each module listed here adheres to the stags specification. That specification is defined at stags/docs/spec.md.

  • superouter A Router that both exposes and internally uses stags to model route definitions, validation and more.

Project Goals and Motivations

  • Serializable
  • 0 Dependencies
  • Tiny for frontend usage
  • Avoid pitfalls found in other sum type libraries

How does stags differ from other libraries in the ecosystem?

stags removes the following features because we believe they lead to brittle codebases.

  • placeholder cases
  • auto spreading of values in cata/fold
  • auto curried constructors
  • prototypes (reference equality checks / instanceof)
  • serializable / useable with Redux/meiosis etc

stags is technically 0KB, it's an idea. You can use stags in your codebase without ever running npm install.

API

either

import { either } from 'stags'

const Loaded = 
    either('Loaded')

either::Y

a -> Either Y a | N b

either::N

b -> Either Y a | N b

either::map

( a -> c ) -> Either Y c | N b

either::bimap

(( a -> c ), ( b -> d )) -> Either Y c | N d

either::bifold

(( a -> c ), ( b -> c )) -> Either Y a | N b -> c

either::getWith

( c , ( b -> c )) -> Either Y a | N b -> c

either::getOr

c -> Either Y a | N b -> c

either::fold

Type -> { Y: a -> c, N: b -> c } -> case -> c

either::chain

( a -> Either Y c | N b ) -> Either Y c | N b

maybe

import { maybe } from 'stags'

const Selected = 
    maybe('Selected')

maybe::Y

a -> Maybe Y a | N

maybe::N

() -> Maybe Y a | N

maybe::map

( a -> c ) -> Maybe Y c | N

maybe::bimap

(( () -> b ), ( a -> b )) -> Maybe Y b | N

maybe::bifold

(( () -> b ), ( a -> b )) -> Maybe Y a | N -> b

maybe::getWith

( b , ( a -> b )) -> Maybe Y a | N -> b

maybe::getOr

b -> Maybe Y a | N -> b

maybe::fold

Type -> { Y: a -> b, N: () -> b } -> case -> b

maybe::chain

( a -> Maybe Y b | N ) -> Maybe Y b | N

Canonical Maybe / Either

In the future some functions will return optional values. This library encourages you to define your own but this library exports two pregenerated Maybe / Either types that can be used canonically as the "real" Maybe or Either which can be helpful when doing natural transformations and conversions between types and safe and unsafe data.

import { Maybe, Either } from stags

const yes = Maybe.Y(100)
const no = Maybe.N()

const f = Maybe.getOr(0)

f(yes)
// => 100

g(no)
// => 0

tagged

import { tagged } from 'stags'

const Geom = 
    tagged ('Geom') ({
        Point: ['x', 'y'],
        Line: ['p1', 'p2'],
        Poly: ['p1', 'p2', 'rest']
    })

const p1 = Geom.Point({ x:0, y: 0 })

const p2 = p1

const line = Geom.Line({ p1, p2 })

const poly = Geom.Poly({ p1, p2, rest: [p3]})

fold

Type -> { [caseName]: a -> b } -> case -> b

foldT

( () -> Type ) -> { [caseName]: a -> b } -> case -> b

Like fold, but receives a function to obtain the type. Useful for defining folds on the type at the time of definition.

map

Type -> { [caseName]: a -> b } -> case -> Type b

⚠ Both map and chain will skip executing cases when their instance has no .value property (usually determined by their type constructor).

chain

Type -> { [caseName]: a -> Type b } -> case -> Type b

⚠ Both map and chain will skip executing cases when their instance has no .value property (usually determined by their type constructor).

But when using map and chain you are still required to pass in a handler for every case.

It's recommended to use otherwise with map and chain to prefill values that are not relevant to the fold.

otherwise

string[] -> f -> { [key:string]: f }

A helper function for generating folds that are versioned separately to the type definition.

const { Y, N } = stags.Maybe
const Platform = stags.tagged ('Platform') ({
    ModernWindows: [],
    XP: [],
    Linux: [],
    Darwin: []
})

// defined separately to detect changes in intent
const rest = stags.otherwise([
    'ModernWindows',
    'XP',
    'Linux',
    'Darwin'
])

const windows = stags.otherwise([
    'ModernWindows',
    'XP'
])

const foldWindows = f => stags.map(Platform) ({
    ... rest(N),
    ... windows( () => Y(f()) )
})

const winPing = 
    foldWindows
        ( () => 'ping \\t www.google.com' )

winPing( Platform.Darwin() )
// => stags.Maybe.N()

winPing( Platform.XP() )
// => stags.Maybe.Y('ping \t www.google.com')

At a later date, you may add support for WSL. Which will likely break earlier assumptions because it's both linux and windows.

const Platform = stags.tagged ('Platform') ({
    ModernWindows: [],
    XP: [],
    WSL: [], // NEW!
    Linux: [],
    Darwin: []
})

Now stags will helpfully throw a MissingCases error for all the usages of our original otherwise functions that no longer discriminate the union.

We can now create a new otherwise for that assumption:

const windows = stags.otherwise([ //OLD
    'ModernWindows',
    'XP'
])

const rest = stags.otherwise([ //OLD
    'ModernWindows',
    'XP',
    'Linux',
    'Darwin'
])

const rest2 = stags.otherwise([ // NEW!
    'ModernWindows',
    'XP',
    'WSL', // NEW
    'Linux',
    'Darwin',
])

const windowsGUI = stags.otherwise([ // NEW
    'ModernWindows',
    'XP',
])

const foldWindowsGUI = f => stags.map(Platform) ({ // NEW
    ... rest2(N),
    ... windowsGUI( () => Y(f()) )
})

const foldWindows = f => stags.map(Platform) ({ // OLD
    ... rest(N),
    ... windows( () => Y(f()) )
})

Our original ping function is using our old function, let's revisit our assumptions:

const winPing = 
    foldWindows
        ( () => 'ping \\t www.google.com' )

const winPing2 =
    foldWindowsGUI
        ( () => 'ping \\t www.google.com' )

When we've updated all the references, stags will stop throwing errors on initialization. You can then delete the old definitions and update the new definitions to have the old names. Leaving us with:

const rest = stags.otherwise([ // renamed
    'ModernWindows',
    'XP',
    'WSL',
    'Linux',
    'Darwin',
])

const windowsGUI = stags.otherwise([
    'ModernWindows',
    'XP',
])

const foldWindowsGUI = f => stags.map(Platform) ({
    ... rest2(N),
    ... windowsGUI( () => Y(f()) )
})

const winPing =
    foldWindowsGUI
        ( () => 'ping \\t www.google.com' )

If we hadn't versioned our otherwise structures separately to the type, we'd get no initialization errors and instead our code would break in unpredictable ways. For example WSL has it's own ping and \t doesn't do anything on the linux version. This is what makes separately versioned placeholders so powerful.

import { foldT } from 'stags'

const Example = {
  name: 'Example'
  A(value){
    return { type: 'Example', case: 'A', value }
  },
  B(){
    return { type: 'Example', case: 'B' }
  },
  // Example doesn't exist at the time of definition
  // So the type is referenced lazily.
  isA: foldT( () => Example ) ({
      A: () => true,
      B: () => false
  })
}

caseName

caseName -> caseName:string

Extract the name of a case from an instance of a type.

foldCase

(b, (a -> b)) -> case a -> b

mapCase

(a -> b) -> case a -> case b

bimapCase

(() -> b, a -> b) -> case -> case b

bifoldCase

(() -> b, a -> b) -> case -> b

chainCase

(a -> case b) -> case a -> case b

Errors

Below is the source code definition for the internal errors this library throws.

const StaticSumTypeError =
    tagged('StaticSumTypeError')({
        ExtraCases: ['extraKeys']
        , MissingCases: ['missingKeys']
        , InstanceNull: ['T']
        , InstanceWrongType: ['T', 'x']
        , InstanceShapeInvalid: ['T', 'x']
        , InvalidCase: ['context']
        , VisitorNotAFunction: ['context', 'visitor']
        , NotAType: ['context', 'T']
    })
ErrorThrows ...
ExtraCaseswhen a fold specifies a visitor for cases that are not of the type.
MissingCaseswhen a fold does not specify a visitor for each case of the type.
InstanceNullwhen an argument was expected to be an instance of a sum type but was instead null.
InstanceWrongTypewhen an instance is a valid stags but not the specifically expected type for that function.
InstanceShapeInvalidwhen an instance has the correct type property but an unknown case property.
VisitorNotAFunctionwhen a function was expected a visitor function but received anything else.
NotATypewhen a function expected a stags type but received anything else.
1.0.0-rc31

4 years ago

1.0.0-rc30

5 years ago

1.0.0-rc29

5 years ago

1.0.0-rc28

5 years ago

1.0.0-rc27

5 years ago

1.0.0-rc26

5 years ago

1.0.0-rc25

5 years ago

1.0.0-rc24

5 years ago

1.0.0-statee-10

5 years ago

1.0.0-statee-9

5 years ago

1.0.0-statee-8

5 years ago

1.0.0-statee-7

5 years ago

1.0.0-statee-6

5 years ago

1.0.0-statee-5

5 years ago

1.0.0-statee-4

5 years ago

1.0.0-statee-3

5 years ago

1.0.0-statee-2

5 years ago

1.0.0-statee-1

5 years ago

1.0.0-rc23

5 years ago

1.0.0-rc20

5 years ago

1.0.0-rc19

5 years ago

1.0.0-rc18

5 years ago

1.0.0-rc16

5 years ago

1.0.0-rc15

5 years ago

1.0.0-rc14

5 years ago

1.0.0-rc13

5 years ago

1.0.0-rc12

5 years ago

1.0.0-rc11

5 years ago

1.0.0-rc9

5 years ago

1.0.0-rc8

5 years ago

1.0.0-rc7

5 years ago

1.0.0-rc6

5 years ago

1.0.0-rc4

5 years ago

1.0.0-rc3

5 years ago

1.0.0

10 years ago

0.0.11

10 years ago

0.0.10

10 years ago

0.0.9

10 years ago

0.0.8

10 years ago

0.0.7

10 years ago

0.0.6

10 years ago

0.0.5

10 years ago

0.0.3

10 years ago

0.0.2

10 years ago

0.0.1

10 years ago