Pratica

Functional Algebraic Data Types

README

🥃 Pratica


Functional Programming for Pragmatists


Why is this for pragmatists you say?

Pratica sacrifices some common FP guidelines in order to provide a simpler and more approachable API that can be used to accomplish your goals quickly - while maintaining data integrity and safety, through algrebraic data types.

For V1 docs - check out v1 docs readme

Install

With yarn
  1. ```sh
  2. yarn add pratica
  3. ```
or if you prefer npm
  1. ```sh
  2. npm i pratica
  3. ```

Changes from V1 to V2


If you are migrating from Pratica V1 to V2. Here is a small list of changes made:

- Maybe() utility was renamed to nullable()
- .default(() => 'value') was renamed to .alt('value') and does not require a function to be passed in, just a value.

That's it. Enjoy.

Monads


Maybe


Use this when dealing with nullable and unreliable data that needs actions performed upon.

Maybe is great for making sure you do not cause runtime errors by accessing data that is not there because of unexpected nulls or undefineds.

Every Maybe can either be of type Just or Nothing. When the data is available, it is wrapped with Just, if the data is missing, it is Nothing. The examples below should clarify futher.

Maybe.map
Map is used for running a function on the data inside the Maybe. Map will only run the function if the Maybe type is Just. If it's Nothing, the map will short circuit and be skipped.
  1. ```js
  2. import { nullable } from 'pratica'

  3. const person = { name: 'Jason', age: 4 }

  4. // Example with real data
  5. nullable(person)
  6.   .map(p => p.age)
  7.   .map(age => age + 5)
  8.   .cata({
  9.     Just: age => console.log(age), // 9
  10.     Nothing: () => console.log(`This function won't run`)
  11.   })

  12. // Example with null data
  13. nullable(null)
  14.   .map(p => p.age) // Maybe type is Nothing, so this function is skipped
  15.   .map(age => age + 5) // Maybe type is Nothing, so this function is skipped
  16.   .cata({
  17.     Just: age => console.log(age), // Maybe type is Nothing, so this function is not run
  18.     Nothing: () => console.log('Could not get age from person') // This function runs because Maybe is Nothing
  19.   })
  20. ```

Maybe.chain
Chain is used when you want to return another Maybe when already inside a Maybe.
  1. ```js
  2. import { nullable } from 'pratica'

  3. const person = { name: 'Jason', age: 4 }

  4. nullable(person)
  5.   .chain(p => nullable(p.height)) // p.height does not exist so nullable returns a Nothing type, any .map, .chain, or .ap after a Nothing will be short circuited
  6.   .map(height => height * 2.2) // this func won't even run because height is Nothing, so `undefined * 2.2` will never execute, preventing problems.
  7.   .cata({
  8.     Just: height => console.log(height), // this function won't run because the height is Nothing
  9.     Nothing: () => console.log('This person has no height')
  10.   })
  11. ```

Maybe.alt
Alt is a clean way of making sure you always return a Just with some default data inside.
  1. ```js
  2. import { nullable } from 'pratica'

  3. // Example with default data
  4. nullable(null)
  5.   .map(p => p.age) // won't run
  6.   .map(age => age + 5) // won't run
  7.   .alt(99) // the data is null so 99 is the default
  8.   .cata({
  9.     Just: age => console.log(age), // 99
  10.     Nothing: () => console.log(`This function won't run because .alt() always returns a Just`)
  11.   })
  12. ```

Maybe.ap
Sometime's working with Maybe can be reptitive to always call .map whenever needing to a apply a function to the contents of the Maybe. Here is an example using .ap to simplify this.

Goal of this example, to perform operations on data inside the Maybe, without unwrapping the data with .map or .chain
  1. ```js
  2. import { Just, nullable } from 'pratica'

  3. // Need something like this
  4. // Just(6) + Just(7) = Just(13)
  5. Just(x => y => x + y)
  6.   .ap(Just(6))
  7.   .ap(Just(7))
  8.   .cata({
  9.     Just: result => console.log(result), // 13
  10.     Nothing: () => console.log(`This function won't run`)
  11.   })

  12. nullable(null) // no function to apply
  13.   .ap(Just(6))
  14.   .ap(Just(7))
  15.   .cata({
  16.     Just: () => console.log(`This function won't run`),
  17.     Nothing: () => console.log(`This function runs`)
  18.   })
  19. ```

Maybe.inspect
Inspect is used for seeing a string respresentation of the Maybe. It is used mostly for Node logging which will automatically call inspect() on objects that have it, but you can use it too for debugging if you like.
  1. ```js
  2. import { nullable } from 'pratica'

  3. const { log } = console

  4. log(nullable(86).inspect()) // `Just(86)`
  5. log(nullable('HELLO').inspect()) // `Just('HELLO')`
  6. log(nullable(null).inspect()) // `Nothing`
  7. log(nullable(undefined).inspect()) // `Nothing`
  8. ```

Maybe.cata
Cata is used at the end of your chain of computations. It is used for getting the final data from the Maybe. You must pass an object to .cata with 2 properties, Just and Nothing (capitalization matters), and both those properties must be a function. Those functions will run based on if the the computations above it return a Just or Nothing data type.

Cata stands for catamorphism and in simple terms means that it extracts a value from inside any container.

  1. ```js
  2. import { Just, Nothing } from 'pratica'

  3. const isOver6Feet = person => person.height > 6
  4.   ? Just(person.height)
  5.   : Nothing

  6. isOver6Feet({ height: 4.5 })
  7.   .map(h => h / 2.2)
  8.   .cata({
  9.     Just: h => console.log(h), // this function doesn't run
  10.     Nothing: () => console.log(`person is not over 6 feet`)
  11.   })
  12. ```

Maybe.toResult
toResult is used for easily converting Maybe's to Result's. Any Maybe that is a Just will be converted to an Ok with the same value inside, and any value that was Nothing will be converted to an Err with no value passed. The cata will have to include Ok and Err instead of Just and Nothing.

  1. ```js
  2. import { Just, Nothing } from 'pratica'

  3. Just(8)
  4.   .toResult()
  5.   .cata({
  6.     Ok: n => console.log(n), // 8
  7.     Err: () => console.log(`No value`) // this function doesn't run
  8.   })

  9. Nothing
  10.   .toResult()
  11.   .cata({
  12.     Ok: n => console.log(n), // this function doesn't run
  13.     Err: () => console.log(`No value`) // this runs
  14.   })
  15. ```

Maybe.isJust
isJust returns a boolean representing the type of the Maybe. If the Maybe is a Just type then true is returned, if it's a Nothing, returns false.

  1. ```js
  2. import { Just, Nothing } from 'pratica'

  3. const isOver6Feet = height => height > 6
  4.   ? Just(height)
  5.   : Nothing

  6. const { log } = console

  7. log(isOver6Feet(7).isJust()) // true
  8. log(isOver6Feet(4).isJust()) // false
  9. ```

Maybe.isNothing
isNothing returns a boolean representing the type of the Maybe. If the Maybe is a Just type then false is returned, if it's a Nothing, returns true.

  1. ```js
  2. import { Just, Nothing } from 'pratica'

  3. const isOver6Feet = height => height > 6
  4.   ? Just(height)
  5.   : Nothing

  6. const { log } = console

  7. log(isOver6Feet(7).isNothing()) // false
  8. log(isOver6Feet(4).isNothing()) // true
  9. ```

Maybe.value
value returns the encapsulated value within the Maybe. If the Maybe is a Just type, then the arg is returned, otherwise, if it is a Nothing, then it returns undefined.

  1. ```js
  2. import { Just, Nothing } from 'pratica'

  3. const isOver6Feet = height => height > 6
  4.   ? Just(height)
  5.   : Nothing

  6. const { log } = console

  7. log(isOver6Feet(7).value()) // 7
  8. log(isOver6Feet(4).value()) // undefined
  9. ```

Result

Use this when dealing with conditional logic. Often a replacment for if statements - or for simplifying complex logic trees. A Result can either be an Ok or an Err type.

Result.map
  1. ```js
  2. import { Ok, Err } from 'pratica'

  3. const person = { name: 'jason', age: 4 }

  4. Ok(person)
  5.   .map(p => p.name)
  6.   .cata({
  7.     Ok: name => console.log(name), // 'jason'
  8.     Err: msg => console.error(msg) // this func does not run
  9.   })
  10. ```

Result.chain

  1. ```js
  2. import { Ok, Err } from 'pratica'

  3. const person = { name: 'Jason', age: 4 }

  4. const isPerson = p => p.name && p.age
  5.   ? Ok(p)
  6.   : Err('Not a person')

  7. const isOlderThan2 = p => p.age > 2
  8.   ? Ok(p)
  9.   : Err('Not older than 2')

  10. const isJason = p => p.name === 'jason'
  11.   ? Ok(p)
  12.   : Err('Not jason')

  13. Ok(person)
  14.   .chain(isPerson)
  15.   .chain(isOlderThan2)
  16.   .chain(isJason)
  17.   .cata({
  18.     Ok: p => console.log('this person satisfies all the checks'),
  19.     Err: msg => console.log(msg) // if any checks return an Err, then this function will be called. If isPerson returns Err, then isOlderThan2 and isJason functions won't even execute, and the err msg would be 'Not a person'
  20.   })
  21. ```

Result.mapErr

You can also modify errors that may return from any result before getting the final result, by using .mapErr or .chainErr.

  1. ```js
  2. import { Err } from 'pratica'

  3. Err('Message:')
  4.   .mapErr(x => x + ' Syntax Error')
  5.   .map(x => x + 7) // ignored because it's an error
  6.   .cata({
  7.     Ok: x => console.log(x), // function not ran
  8.     Err: x => console.log(x) // 'Message: Syntax Error'
  9.   })
  10. ```

Result.chainErr
  1. ```js
  2. import { Err } from 'pratica'

  3. Err('Message:')
  4.   .chainErr(x => x + Err(' Syntax Error'))
  5.   .map(x => x + 7) // ignored because it's an error
  6.   .cata({
  7.     Ok: x => console.log(x), // function not ran
  8.     Err: x => console.log(x) // 'Message: Syntax Error'
  9.   })
  10. ```

Result.swap
Use .swap() to convert an Err to an Ok, or an Ok to an Err.
  1. ```js
  2. import { Ok } from 'pratica'

  3. Ok('hello')
  4.   .swap()
  5.   .cata({
  6.     Ok: () => console.log(`doesn't run`),
  7.     Err: x => expect(x).toBe('hello') // true
  8.   })
  9. ```

Result.bimap
Use .bimap() for easily modifying an Ok or an Err. Shorthand for providing both .map and .mapErr
  1. ```js
  2. import { Ok } from 'pratica'

  3. Ok('hello')
  4.   .bimap(x => x + ' world', x => x + ' goodbye')
  5.   .cata({
  6.     Ok: x => expect(x).toBe('hello world'), // true
  7.     Err: () => {}
  8.   })

  9. Err('hello')
  10.   .bimap(x => x + ' world', x => x + ' goodbye')
  11.   .cata({
  12.     Ok: () => {},
  13.     Err: x => expect(x).toBe('hello goodbye') // true
  14.   })
  15. ```

Result.ap
  1. ```js
  2. import { Ok } from 'pratica'

  3. // Need something like this
  4. // Ok(6) + Ok(7) = Ok(13)
  5. Ok(x => y => x + y)
  6.   .ap(Ok(6))
  7.   .ap(Ok(7))
  8.   .cata({
  9.     Ok: result => console.log(result), // 13
  10.     Err: () => console.log(`This function won't run`)
  11.   })

  12. Ok(null) // no function to apply
  13.   .ap(Ok(6))
  14.   .ap(Ok(7))
  15.   .cata({
  16.     Ok: () => console.log(`This function won't run`),
  17.     Err: () => console.log(`This function runs`)
  18.   })
  19. ```

Result.inspect
  1. ```js
  2. import { Ok, Err } from 'pratica'

  3. const { log } = console

  4. log(Ok(86).inspect()) // `Ok(86)`
  5. log(Ok('HELLO').inspect()) // `Ok('HELLO')`
  6. log(Err('Something happened').inspect()) // `Err('Something happened')`
  7. log(Err(404).inspect()) // `Err(404)`
  8. ```

Result.cata
  1. ```js
  2. import { Ok, Err } from 'pratica'

  3. const isOver6Feet = person => person.height > 6
  4.   ? Ok(person.height)
  5.   : Err('person is not over 6 feet')

  6. isOver6Feet({ height: 4.5 })
  7.   .map(h => h / 2.2)
  8.   .cata({
  9.     Ok: h => console.log(h), // this function doesn't run
  10.     Err: msg => console.log(msg) // `person is not over 6 feet`
  11.   })
  12. ```

Result.toMaybe
toMaybe is used for easily converting Result's to Maybe's. Any Result that is an Ok will be converted to a Just with the same value inside, and any value that was Err will be converted to a Nothing with no value passed. The cata will have to include Just and Nothing instead of Ok and Err.

  1. ```js
  2. import { Ok, Err } from 'pratica'

  3. Ok(8)
  4.   .toMaybe()
  5.   .cata({
  6.     Just: n => console.log(n), // 8
  7.     Nothing: () => console.log(`No value`) // this function doesn't run
  8.   })

  9. Err(8)
  10.   .toMaybe()
  11.   .cata({
  12.     Just: n => console.log(n), // this function doesn't run
  13.     Nothing: () => console.log(`No value`) // this runs
  14.   })
  15. ```

Result.isOk
  1. ```js
  2. import { Ok, Err } from 'pratica'

  3. const isOver6Feet = height => height > 6
  4.   ? Ok(height)
  5.   : Err('Shorty')

  6. const { log } = console

  7. log(isOver6Feet(7).isOk()) // true
  8. log(isOver6Feet(4).isOk()) // false
  9. ```

Result.isErr
  1. ```js
  2. import { Ok, Err } from 'pratica'

  3. const isOver6Feet = height => height > 6
  4.   ? Ok(height)
  5.   : Err('Shorty')

  6. const { log } = console

  7. log(isOver6Feet(7).isErr()) // false
  8. log(isOver6Feet(4).isErr()) // true
  9. ```

Result.value
Returns either the value contained in the Ok, or the error in the Err

  1. ```js
  2. import { Ok, Err } from 'pratica'

  3. const six = Ok(6).value()
  4. const error = Err('Something happened').value()

  5. log(six) // 6
  6. log(error) // 'Something happened'
  7. ```

Utilities

parseDate

Safely parse date strings. parseDate returns a Maybe monad.
  1. ```js
  2. import { parseDate } from 'pratica'

  3. const goodDate = '2019-02-13T21:04:10.984Z'
  4. const badDate = '2019-02-13T21:04:1'

  5. parseDate(goodDate).cata({
  6.   Just: date => expect(date.toISOString()).toBe(goodDate),
  7.   Nothing: () => console.log('could not parse date string') // this function doesn't run
  8. })

  9. parseDate(badDate).cata({
  10.   Just: () => console.log(`this function doesn't run`),
  11.   Nothing: () => 'this function runs'
  12. })

  13. // it's a maybe, so you can use chain/default/ap
  14. parseDate(null)
  15.   .default(() => new Date())
  16.   .cata({
  17.     Just: date => date.toISOString(), // this runs
  18.     Nothing: () => `doesn't run because of the .default()`
  19.   })
  20. ```

encase

Safely run functions that may throw an error or crash. encase returns a Maybe type (so Just or Nothing).
  1. ```js
  2. import { encase } from 'pratica'

  3. const throwableFunc = () => JSON.parse('<>')

  4. // this func doesn't throw, so Just is called
  5. encase(() => 'hello').cata({
  6.   Just: x => console.log(x), // hello
  7.   Nothing: () => console.log('func threw error') // this func doesn't run
  8. })

  9. // this function throws an error so Nothing is called
  10. encase(throwableFunc).cata({
  11.   Just: json => console.log(`doesn't run`),
  12.   Nothing: () => console.error('func threw an error') // this runs
  13. })
  14. ```

encaseRes

Safely run functions that may throw an error or crash. encaseRes returns a Result type (so Ok or Err). Similar to encase but the Err returns the error message.
  1. ```js
  2. import { encaseRes } from 'pratica'

  3. const throwableFunc = () => JSON.parse('<>')

  4. // this func doesn't throw, so Ok is called
  5. encaseRes(() => 'hello').cata({
  6.   Ok: x => console.log(x), // hello
  7.   Err: () => console.log('func threw error') // this func doesn't run
  8. })

  9. // this function throws an error so Err is called
  10. encaseRes(throwableFunc).cata({
  11.   Ok: json => console.log(`doesn't run`),
  12.   Err: msg => console.error(msg) // SyntaxError: Unexpected token < in JSON at position 0
  13. })
  14. ```

justs

Filter out any non-Just data type from an array

  1. ```js
  2. import { justs } from 'pratica'

  3. const data = [1, true, Just('hello'), Nothing, Ok('hey'), Err('No good')]

  4. justs(data) // returns [Just('hello')]
  5. ```

oks

Filter out any non-Ok data type from an array

  1. ```js
  2. import { oks } from 'pratica'

  3. const data = [1, true, Just('hello'), Nothing, Ok('hey'), Err('No good')]

  4. oks(data) // returns [Ok('hey')]
  5. ```

get

Safely retrieve a nested property in an object. Returns a Maybe.
  1. ```js
  2. import { get } from 'pratica'

  3. const data = {
  4.   name: 'jason',
  5.   children: [
  6.     {
  7.       name: 'bob'
  8.     },
  9.     {
  10.       name: 'blanche',
  11.       children: [
  12.         {
  13.           name: 'lera'
  14.         }
  15.       ]
  16.     }
  17.   ]
  18. }

  19. get(['children', 1, 'children', 0, 'name'])(data).cata({
  20.   Just: name => expect(name).toBe('lera'), // true
  21.   Nothing: () => console.log('no name') // doesn't run
  22. })
  23. ```

head

Safely get the first item in an array. Returns a Maybe.
  1. ```js
  2. import { head } from 'pratica'

  3. const data = [5,1,2]

  4. // example with data
  5. head(data)
  6.   .cata({
  7.     Just: x => expect(x).toBe(5), // true,
  8.     Nothing: () => console.log('No head') // won't run
  9.   })

  10. // example with empty data
  11. head([])
  12.   .cata({
  13.     Just: x => console.log(x), // doesn't run
  14.     Nothing: () => console.log('No head') // runs
  15.   })
  16. ```

last

Safely get the last item in an array. Returns a Maybe.
  1. ```js
  2. import { last } from 'pratica'

  3. const data = [5,1,2]

  4. // example with data
  5. last(data)
  6.   .cata({
  7.     Just: x => expect(x).toBe(2), // true,
  8.     Nothing: () => console.log('No last') // won't run
  9.   })

  10. // example with empty data
  11. last([])
  12.   .cata({
  13.     Just: x => console.log(x), // doesn't run
  14.     Nothing: () => console.log('No last') // runs
  15.   })
  16. ```

tail

Safely get the tail of an array (Everything except the first element). Returns a Maybe.
  1. ```js
  2. import { tail } from 'pratica'

  3. const data = [5,1,2]

  4. // example with data
  5. tail(data)
  6.   .cata({
  7.     Just: x => expect(x).toEqual([1,2]), // true,
  8.     Nothing: () => console.log('No tail') // won't run
  9.   })

  10. // example with empty data
  11. last([])
  12.   .cata({
  13.     Just: x => console.log(x), // doesn't run
  14.     Nothing: () => console.log('No tail') // runs
  15.   })
  16. ```

tryFind

Safely try to retrieve an item from an array. Returns a Maybe.

  1. ```js
  2. import { tryFind } from 'pratica'

  3. const users = [
  4.   {name: 'jason', age: 6, id: '123abc'},
  5.   {name: 'bob', age: 68, id: '456def'}
  6. ]

  7. tryFind(u => u.id === '123abc')(users)
  8.   .cata({
  9.     Just: user => expect(user).toEqual(users[0]), // true
  10.     Nothing: () => 'Could not find user with id 123abc' // doesn't run
  11.   })
  12. ```