- Published on
Functional Programming with TypeScript
- Authors
- Name
- Garfield Zhu
- @_AlohaYo_
@Author: Garfield Zhu
As a declarative programming paradigm, Functional Programming leverages the math theory and type system to build the program with pure functions in a modular manner.
As a good language supporting functional paradigm, as well as a strong type system, TypeScript is ideal for practising FP. In this article, we just introduce and demo some useful snippets of FP in TypeScript.
Related libraries:
fp-ts is the most recommended to be used in ts. Read code conventions for a best practise.
Run with Deno runtime, like:
deno run memoize.ts
ramda.js
is one of the common FP library for js. Refer to official doc and a tutorial in Chinese.
Keep IMMUTABLE
The best practise in FP is using immutable variables anywhere if possible.
In TypeScript, use const
for type declaration and readonly
for properties of type/interface and args of functions as much as possible. Avoid using let
or var
declaration if they are not a must. (Actually, there should always be a way to abandon a mutable variable)
Example:
declare const a : number
const b = a > 0 ? 'positive' : 'negativeOrZero' // $ExpectedType: 'positive' | 'negativeOrZero'
let b = a > 0 ? 'postive' : 'negativeOrZero' // $ExpectedType: string
Pure function
Cacheable
Portable / Self-documenting
Testable
Reasonable
Concurrency
Curry
TypeScript is not born to support curry. Node package like [Rambda](https://github.com/ramda/ramda)
provides a good implementation of curry, but since we are on Deno
with TypeScript, it seems not very direct for us to use it, neither give a cool implementation of it in TS.
A sample for a currying step by step: How to master advanced TypeScript patterns
Also, we can look forward for the proposed feature Variadic Kinds, which will make it easier to define a typed curry
function.
Fortunately, Denofun serves a group of useful utilities for FP including curry
.
Example:
import curry from "https://deno.land/x/denofun/lib/curry.ts";
const greet = (name: string, age: number) => `hello, I'm ${name} and I'm ${age} years old`;
const curriedGreet = curry(greet);
curriedGreet("Tomasz")(26)
Practises: using curry
Compose
Associativity
const associative = compose(f, compose(g, h)) == compose(compose(f, g), h) // Both have: // associative(x) = f(g(h(x)))
Pointfree "Never say your data"
// not pointfree, for data `word` is mentioned const snakeCase = (word: string) => word.toLowerCase().replace(/\s+/ig, '_') // pointfree const pointfreeSnakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);
- Reference: Favoring Curry
- Blog: using pointfree
Debug It could be hard to find the error place in a pointfree style, since we throw all the parameters away Like this:
const dasherize = compose(join('-'), toLower, split(' '), replace(/\s{2,}/ig, ' ')) dasherize('The world is a vampire') // TypeError: Cannot read property 'apply' of undefined
We can simply use a curried
trace
function to telemetry the parameters for debugging:const trace = curry(function(tag, x){ console.log(tag, x) return x }) const dasherize = compose(join('-'), toLower, trace("after split"), split(' '), replace(/\s{2,}/ig, ' ')); // after split [ 'The', 'world', 'is', 'a', 'vampire' ] // Correct: const dasherize = compose(join('-'), map(toLower), split(' '), replace(/\s{2,}/ig, ' ')) dasherize('The world is a vampire');
Moreover, we have a similar pipe
function, which also combines the functions. The function parameter is called from right to left in compose
, but left to right in pipe
.
Practises: using compose
Functor
A Functor is a type that implements map and obeys some laws
Typically, we use pointed function which contains a of
function for instantiating a functor.
Container.of(3)
// Container(3)
Container.of(3).map(x => x + 1)
// Container(4)
Applicative Functor
"Applicative functor" is a kind of "pointed functor" which implements ap
function.
Function ap
should apply the value of a functor to the value of another functor.
It means: F.of(x).map(f) == F.of(f).ap(F.of(x))
Container.of(add(2)).ap(Container.of(3))
// Container(5)
Container.of(2).map(add).ap(Container.of(3))
Use applicative functor will make a curried function in functor being used like a a normal function call:
Maybe.of(add).ap(Maybe.of(2)).ap(Maybe.of(3))
// Maybe(5)
Task.of(add).ap(Task.of(2)).ap(Task.of(3));
// Task(5)
Reference
Mostly adequate guide to FP (in javascript)