functir
v1.3.1
Published
Functional programming library for JavaScript. True FP in JS/TS!
Downloads
15
Maintainers
Readme
About
Functir is a functional programming library for JavaScript. True functional programming in JS!
Installation
Core data types
Core features
Installation
via pnpm:
pnpm add functir
via npm:
npm i functir
Core data types
Box
Box is super simple – it's class that containing some immutable value. You cannot change box wrapped value directly, but you can transform it and get new Box instance.
import { Box } from 'functir'
// 1. Let's create wrapped number value
const wrapped = new Box(1)
console.log(wrapped.value) // 1
// 2. Let's change it (immutable!)
const wrappedTransformed = wrapped.mutate(_ => _ + 100)
console.log(wrappedTransformed.value) // 101
Other types described in future also will also use Box
to contain some value via class construction.
Option
Mostly Option
data type is used in pattern matching (described in future). Option
is primary designed to be one of two values: None
or Some(value)
. Some
value is works same as Box
– wraps some value.
import { Option, None, Some } from 'functir'
// Both of values is Option<number>
const valueOne: Option<number> = None()
const valueTwo: Option<number> = Some(200)
// You can mutate value of `Some` by converting to `Box`
console.log(valueTwo.asBox) // Box(200)
console.log(valueTwo.asBox.mutate(_ => _ + 5)) // Box(205)
Either
The Either<TLeft, TRight>
is very similar to Option
, but the value can be Left(value: TLeft)
or Right(value: TRight)
:
import { Either, Left, Right } from 'functir'
// Either<number, string> = Left<number> | Right<string>
// Both of values is Either<number, string>
const valueOne: Either<number, string> = Left(100)
const valueTwo: Either<number, string> = Right('200')
// You can mutate value by converting to `Box`
console.log(valueOne.asBox) // Box(100)
console.log(valueOne.asBox.mutate(_ => _ + 5)) // Box(105)
LikeBox
What does Box
, Option
, Either
have in common? - they are all implements LikeBox
interface, but in different variations.
We have 3 main variations of LikeBox
interface:
LikeBox
(None
type uses it, it doesn't have wrapped value inside)LikeFilledBox
(Box
type uses it, inheritsLikeBox
but have wrapped value inside)LikeConvertibleFilledBox
(Option
&Either
types uses it, inheritsLikeFilledBox
but haveasBox
converter inside)
How you can use those interfaces? Like that:
import { Box } from 'functir'
class SomeValue extends Box.filled<number> {}
const wrapped = new SomeValue(200)
console.log(wrapped.value) // 200
console.log(wrapped.asBox) // Box(200)
Illustrated code produces for you a LikeConvertibleFilledBox
class with wrapped number
, that you can use. The same thing does None
, Some
, Left
, Right
implementation.
Now let's see what functions does LikeBox
provides:
Usage: match
/pipe
Shorthand for using pattern matching or piping:
import { Option, None, Some, match, is } from 'functir'
const boxNone: Option<number> = None()
const boxSome: Option<number> = Some(200)
// `.match`/`.pipe` – this functions is works on
// `Box`, `Option`, `Either`, `Box.filled<T>`
// becase they are `LikeBox` implementations
// Pattern matching
boxNone.match([
is(None, _ => "none"),
is(Some, _ => "some")
]) // none
boxSome.match([
is(None, _ => "none"),
is(Some, _ => "some")
]) // some
// Piping
boxSome.pipe([
_ => _ + 100, // 200 + 100 = 300
_ => _ + 50 // 300 + 50 = 350
]) // 350
Usage: flatten
This magic function is just flattens wrapped LikeBox
values:
import { Box, Some } from 'functir'
// `Box` inside `Box` nested
const deep = new Box(new Box(5))
console.log(deep.flatten()) // 5
// Different `LikeBox` implementations nested
const complexDeep = new Box(new Some(new Box(new Some(20))))
console.log(complexDeep.flatten()) // 20
Seq
Seq
(Sequence) is helpful data type that you can use as alternative for arrays. Why Seq
instead of arrays?
Seq
is fully immutable-safe (doesn't provides any mutable methods)Seq
provides a lot of methods that arrays doesn't
Usage is simple:
import { Seq } from 'functir'
const seq = new Seq<number>(1, 2, 3)
console.log(seq.asArray) // [1, 2, 3]
Usage: methods
Seq
provides many different immutable methods you can use:
// Just copies current sequence
seq.copy() // Seq(1, 2, 3)
// Converting into array, set, map
seq.asArray // [1, 2, 3]
seq.asSet // Set(1, 2, 3)
seq.asMap // Map({ 0: 1, 1: 2, 2: 3 })
// Adds value to start of seq
seq.prepended(10) // Seq(10, 1, 2, 3)
// Adds value to end of seq
seq.appended(10) // Seq(1, 2, 3, 10)
// Auto sorting, like [].sort()
seq.autoSorted(0, 10) // Seq(1, 2, 3)
// Sort using predicate
seq.sorted((a, b) => (a > b ? -1 : 1)) // Seq(3, 2, 1)
// Reverses values
seq.reversed() // Seq(3, 2, 1)
// Maps values
seq.mapped(_ => _ + 10) // Seq(11, 12, 13)
// Filters values
seq.filtered(_ => _ > 1) // Seq(2, 3)
// Pads from start with value (to length of 6)
seq.padStart(6, -1) // Seq(-1, -1, -1, 1, 2, 3)
// Pads from end with value (to length of 6)
seq.padEnd(6, -1) // Seq(1, 2, 3, -1, -1, -1)
// Get index of item (from start)
new Seq(1, 1, 1).indexOf(1) // 0
// Get index of item (from end)
new Seq(1, 1, 1).lastIndexOf(1) // 2
Trait
Trait is the concept from Scala
/Rust
languages. Using Trait
you can easily create class (DTO/DAO) for some data model:
import { Trait } from 'functir'
// Create a class using trait
// Trait will automatically create immutable fields, constructor for the class
const UserDTO = Trait<{
readonly nickname: string
readonly age: number
}>();
// Create instance of trait
// All fields of trait should be passed to constructor
const jake = new UserDTO({
nickname: 'Jake',
age: 20
})
// You can convert trait instances into objects or `Box`
console.log(jake.asObject) // { nickname: 'Jake', age: 20 }
console.log(jake.asBox) // Box({ nickname: 'Jake', age: 20 )
Trait
of course produces immutable classes (with copy
method), you can use that for creating services:
import { Trait, Seq } from 'functir'
/**
* Immutable service that builds pizza
*/
class PizzaService extends Trait<{
readonly size: 'sm' | 'md' | 'lg';
readonly toppings: Seq<'meat' | 'pineapple' | 'cheese'>;
}>() {
constructor() {
super({
size: 'sm',
toppings: new Seq()
})
}
public setSize = (size: 'sm' | 'md' | 'lg') =>
this.copy({ size })
public addTopping = (topping: 'meat' | 'pineapple' | 'cheese') =>
this.copy({ toppings: this.toppings.appended(topping) })
}
// Immutability! Let's build pizza
const myPizza = new PizzaService()
.setSize('md')
.addTopping('meat')
.addTopping('cheese')
.asObject
console.log(myPizza) // { size: 'md', toppings: ['meat', 'cheese'] }
IO
IO
is also another helpful type for defining input and output of function:
import { IO } from 'functir'
// Operation that converts string to number
const toNumber: IO<string, number> =
_ => parseInt(_)
console.log(toNumber('123')) // 123
Also if function is async you can use AsyncIO
:
import { AsyncIO } from 'functir'
// Operation that async (just return input as output)
const someFunction: AsyncIO<string, string> =
async _ => _
There's third parameter in IO that we didn't mentioned, it's used to define what Throwable
(described below) can be given by function:
import { IO, ThrowableTrait } from 'functir'
// Our own throwable error
class TooLongError extends ThrowableTrait('MyOwnError', 'Value was too long') {}
// Our operation described:
// input = string, output = string, throws = MyOwnError
// if input length > 5 we return it, otherwise TooLongError given
const someFunction: IO<string, string, TooLongError> =
_ => _.length > 5 ? _ : new TooLongError
Throwable
The Throwable
is the helpful type used with IO
to annotate what error can happen in a function:
import { Throwable, ThrowableTrait } from 'functir'
// Simple error
const someError: Throwable = new Error('Some custom throwed error')
Also you can create own throwable error class using ThrowableTrait
:
import { Throwable, ThrowableTrait } from 'functir'
// Create own throwable error
class CustomError extends ThrowableTrait('CustomError') {}
// Our custom error
const someError2: Throwable = new CustomError('Some super custom error')
Core features
Pattern matching
Pattern matching if well-known pattern that allows to match value to a different cases like in a switch-case
, but more clean syntax and ability to check for instances of classes.
Example:
import { match, is, _, Some, None } from 'functir'
// 1 if Some
// 0 if None
// -1 otherwise
const result = match(Some(100))([
is(Some, _ => 1),
is(None, _ => 0),
is(_, _ => -1)
])
console.log(result) // 1
The _
is special symbol used to handle case none of cases is matched.
Piping
Will be described later.