radash
radash copied to clipboard
Add a pipe function
The pipe function would be like chain but instead of returning a function, it would pass the value through and execute the function.
Logically, pipe is a way of executing many functions, one after another.
See this article for a detailed concept.
The Interface
import { pipe } from 'radash'
const addFive = (num) => num + 5
const divideByTwo = (num) num / 2
pipe(5, addFive, divideByTwo) // 5
This made me consider a skip function. Some functions have a side effect and return nothing, you might want to run it in a pipe but not break the value passing through the pipe.
const skip = (func) => (value) => {
func(value)
return value
}
pipe(5, addFive, console.log, divideByTwo) // errors
pipe(5, addFive, skip(console.log), divideByTwo) // console.logs 10 and returns 5
I want to make a PR for this, but I don't know where to add the code. Can I add to curry.ts? Or should I create a new file for it?
Also if pipe's params contain async functions, pipe should be async, should I move it to async.ts?
import isPromise from 'is-promise'
type AsPromise<T> = T extends Promise<any> ? T : Promise<T>
type MayBePromiseReturn<O, T> = Extract<O, Promise<any>> extends never ? T : AsPromise<T>
type PromiseValue<T> = T extends Promise<infer U> ? U : T
export function pipe<T0, T1, T2, T3, T4, T5, T6, T7>(src: T0,
op1: ((s: PromiseValue<T0>) => T1),
op2: ((s: PromiseValue<T1>) => T2),
op3: ((s: PromiseValue<T2>) => T3),
op4: ((s: PromiseValue<T3>) => T4),
op5: ((s: PromiseValue<T4>) => T5),
op6: ((s: PromiseValue<T5>) => T6),
op7: ((s: PromiseValue<T6>) => T7),
): MayBePromiseReturn<T0 | T1 | T2 | T3 | T4 | T5 | T6 | T7, T7>
export function pipe<T0, T1, T2, T3, T4, T5, T6>(src: T0,
op1: ((s: PromiseValue<T0>) => T1),
op2: ((s: PromiseValue<T1>) => T2),
op3: ((s: PromiseValue<T2>) => T3),
op4: ((s: PromiseValue<T3>) => T4),
op5: ((s: PromiseValue<T4>) => T5),
op6: ((s: PromiseValue<T5>) => T6),
): MayBePromiseReturn<T0 | T1 | T2 | T3 | T4 | T5 | T6, T6>
export function pipe<T0, T1, T2, T3, T4, T5>(src: T0,
op1: ((s: PromiseValue<T0>) => T1),
op2: ((s: PromiseValue<T1>) => T2),
op3: ((s: PromiseValue<T2>) => T3),
op4: ((s: PromiseValue<T3>) => T4),
op5: ((s: PromiseValue<T4>) => T5),
): MayBePromiseReturn<T0 | T1 | T2 | T3 | T4 | T5, T5>
export function pipe<T0, T1, T2, T3, T4>(src: T0,
op1: ((s: PromiseValue<T0>) => T1),
op2: ((s: PromiseValue<T1>) => T2),
op3: ((s: PromiseValue<T2>) => T3),
op4: ((s: PromiseValue<T3>) => T4),
): MayBePromiseReturn<T0 | T1 | T2 | T3 | T4, T4>
export function pipe<T0, T1, T2, T3>(src: T0,
op1: ((s: PromiseValue<T0>) => T1),
op2: ((s: PromiseValue<T1>) => T2),
op3: ((s: PromiseValue<T2>) => T3),
): MayBePromiseReturn<T0 | T1 | T2 | T3, T3>
export function pipe<T0, T1, T2>(src: T0,
op1: ((s: PromiseValue<T0>) => T1),
op2: ((s: PromiseValue<T1>) => T2),
): MayBePromiseReturn<T0 | T1 | T2, T2>
export function pipe<T0, T1>(src: T0,
op1: ((s: PromiseValue<T0>) => T1),
): MayBePromiseReturn<T0 | T1, T1>
export function pipe(src: any, ...ops: ((s: any) => any)[]): any {
let value = src
for (const op of ops) {
if (isPromise(value))
value = value.then(x => op(x))
else
value = op(value)
}
return value
}
I have made one for myself. It can resolve promise automatically and return promise if one of the operators is async. Happy to share.
export function $<T, Args extends readonly unknown[], R>(fn: (value: T, ...args: Args) => R) {
return (...args: Args) => {
return (value: T) => {
return fn(value, ...args)
}
}
}
export function take<T>(collection: T[], size: number) {
return collection.slice(0, size)
}
export function filter<T>(collection: T[], predicate: (value: T) => boolean) {
return collection.filter(predicate)
}
with these kinds of operators, we can do something like this with strict typings and hints:
const result = take(source, 5)
const result = filter(source, x => x.bool)
// pipe
pipe(value, $(filter)(x => x.bool), $(take)(5))
pipe(value, $(filter)(x => x.bool), $(take)( //...
// size: number
pipe(value, $(filter)(x => x.bool), $(take)('str'))
// Argument of type 'string' is not assignable to parameter of type 'number'.
radash operators can be smoothly applied in this pipe and $.
more operators are here:
export function bind<T extends Record<string | symbol, any>>(_this: T): T {
return new Proxy<T>(_this, {
get(target: T, p: string | symbol, receiver: any): any {
if (target[p] instanceof Function)
return target[p].bind(target)
return target[p]
},
})
}
export function mapValues<K extends PropertyKey, T, R>(obj: Record<K, T>, selector: (value: T, key: K) => R) {
const newObj: Record<PropertyKey, R> = {}
for (const [key, value] of entries(obj)) {
newObj[key] = selector(value, key)
}
return newObj as Record<K, R>
}
export function groupBy<T, K extends PropertyKey>(collection: T[], selector: (value: T) => K) {
const obj: Record<PropertyKey, T[]> = {}
for (const value of collection) {
const key = selector(value)
obj[key] = obj[key] || []
obj[key].push(value)
}
return obj as Record<K, T[]>
}
export function flatMap<T, R>(collection: T[], fn: (value: T) => R[]) {
return collection.flatMap(fn)
}
export function map<T, R>(collection: T[], fn: (value: T) => R) {
return collection.map(fn)
}
export function min(obj: number[]): number {
return Math.min(...obj)
}
export function max(obj: number[]): number {
return Math.max(...obj)
}
export function sum(obj: number[]): number {
return obj.reduce((p, x) => p + x, 0)
}
export function mean(obj: number[]): number {
return sum(obj) / obj.length
}
export function size<T>(obj: T[]): number {
return obj.length
}
export function keys<K extends PropertyKey, T>(obj: Record<K, T>) {
return Object.keys(obj) as K[]
}
export function values<K extends PropertyKey, T>(obj: Record<K, T>) {
return Object.values(obj) as T[]
}
export function entries<K extends PropertyKey, T>(obj: Record<K, T>) {
return Object.entries(obj) as ([K, T])[]
}
export function pickBy<K extends PropertyKey, T>(obj: Record<K, T>, predicate: (value: T) => boolean) {
const newObj: Record<PropertyKey, T> = {}
for (const [key, value] of entries(obj)) {
if (predicate(value))
newObj[key] = value
}
return newObj as Record<K, T>
}
export function keyBy<K extends PropertyKey, T>(collection: T[], selector: (value: T) => K) {
const obj: Record<PropertyKey, T> = {}
for (const value of collection) {
const key = selector(value)
obj[key] = value
}
return obj as Record<K, T>
}
export function merge<K extends PropertyKey, T, K2 extends PropertyKey, T2>(obj: Record<K, T>, ...sources: Record<K2, T2>[]) {
const result: Record<PropertyKey, any> = { ... obj }
for (const source of sources) {
for (const [key, value] of entries(source)) {
if (isObject(result[key]) && isObject(value)) {
result[key] = merge(result[key], value)
} else {
result[key] = value
}
}
}
return result as Record<K | K2, T & T2>
}
export function take<T>(collection: T[], size: number) {
return collection.slice(0, size)
}
export function filter<T>(collection: T[], predicate: (value: T) => boolean) {
return collection.filter(predicate)
}
export function chunk<T>(collection: T[], size: number) {
const newCollection = []
for(let i = 0; i < collection.length; i += size) {
newCollection.push(collection.slice(i, i + size))
}
return newCollection
}
export function orderBy<T>(collection: T[], iteratees: MayBeArray<(value: T) => number>, orders: MayBeArray<('desc' | 'asc')> = 'asc') {
const iterateesArr = isArray(iteratees) ? iteratees : [iteratees]
const ordersArr = isArray(orders) ? orders : [orders]
const count = iterateesArr.length
return collection.map(x => ({
value: x,
criteria: iterateesArr.map(iteratee => iteratee(x))
})).sort((a, b) => {
for (let index = 0; index < count; index++) {
const aV = a.criteria[index]
const bV = b.criteria[index]
if (aV === bV)
continue
const order = ordersArr[index] ?? 'asc'
return (order === 'desc' ? aV > bV : aV < bV) ? -1 : 1
}
return 0
}).map(x => x.value)
}
@haoran965 thanks so much for wanting to contribute ❤️ I think the curry.ts module is a great place to add pipe 👍 check out the compose function and its tests, its sort of magic because it works on sync and async functions. If we can make that possible for pipe that would be awesome (and we can leave it in curry.ts).
@shtse8 thank you for sharing your pipe function, I love how simple it is! The $ function is also great. I'd love to see a PR where I can run tests and play with the typing/hints in my IDE. One thing we would need to do is build our own is-promise check so we don't add a dependency.
@shtse8 thank you for sharing your pipe function, I love how simple it is! The
$function is also great. I'd love to see a PR where I can run tests and play with the typing/hints in my IDE. One thing we would need to do is build our ownis-promisecheck so we don't add a dependency.
I am working on a big project, so I don't have much time to submit PRs shortly. hope @haoran965 can help.
do you mind if i do it @shtse8 ? It would make me a PR for Hacktoberfest 😄
do you mind if i do it @shtse8 ? It would make me a PR for Hacktoberfest 😄
please do. 🚀🚀
I tried to design something like: to wrap the pipe and $ methods like rxjs which may be widely used by others probably.
export class Dash<T> {
constructor(private readonly _value: T) {
}
_<Args extends readonly [], R>(fn: (value: T, ...args: Args) => R, ...args: Args) {
return new Dash<R>(fn(this._value, ...args))
}
pipe<R>(fn: (value: T) => R) {
return new Dash<R>(fn(this._value))
}
value() {
return this._value
}
}
It doesn't work. the typing is messed up. It should work, but no idea why.
That's interesting @shtse8 looks like you're going in more of a stream/reactive direction. There could be value in that type of function/feature/implementation but I was thinking of a simple function tool that lets you run an initial value through a series of operations.
const pipe = (value, ...funcs) => funcs.reduce((acc, fn) => fn(acc), value)
const add = (n) => (num) => num + n
const minus = (n) => (num) => num - n
const multiply = (n) => (num) => num * n
const divide = (n) => (num) => num / n
const round = (num) => Math.round(num)
const suffix = (str) => (value) => `${value}${str}`
const fixed = (num) => +num.toFixed()
const debug = (func) => (value) => { func(value); return value }
pipe(5, add(5), divide(2)) // => 5
pipe(5, add(5), divide(3), round, suffix('/mph')) // => '3/mph'
pipe(5, add(5), debug(divide(2)), suffix('/mph')) // => prints 10 and returns '5/mph'
It's still simple, I'd be interested in making it usable with async or maybe providing builtin helper functions for running through pipes. A real use case might be:
const toCelsius = (f) => pipe(f,
minus(32),
multiply(5),
divide(9),
fixed,
suffix(' °C'))
toCelsius(32) // => 0 °C
toCelsius(212) // => 100 °C
toCelsius(90) // => 32 °C
I was investigating to use Radash for one of my projects, but I am also missing the possibility to do easy type-safe chaining.
I created this working proof-of-concept: https://stackblitz.com/edit/typescript-fgyues?file=index.ts
it uses Radash, I borrowed the .pipe() function of fp-ts and added some glue code to lift the existing Radash functions to be easily applied in the chain.
Example usage:
import * as _ from 'radash';
import { unique, alphabetical } from 'radash/operators';
// chained functions (with type-safety)
console.log(
_.pipe(
fish,
(it) => _.unique(it, (f) => f.name),
(it) => _.alphabetical(it, (f) => f.name, 'desc')
)
);
// chained functions, operator-style (with type-safety)
console.log(
_.pipe(
fish,
unique((f) => f.name),
alphabetical((f) => f.name, 'desc')
)
);
Implementation is farely easy:
function buildOperator<FIRST_ARG extends unknown, OTHER_ARGS extends unknown[], RETURN>(
radashFn: (firstArg: FIRST_ARG, ...args: OTHER_ARGS) => RETURN
): (...otherArgs: OTHER_ARGS) => (firstArg: FIRST_ARG) => RETURN {
return (...otherArgs: OTHER_ARGS) => {
return (firstArg: FIRST_ARG) => radashFn(firstArg, ...otherArgs);
}
}
export const unique = buildOperator(rUnique);
export const alphabetical = buildOperator(rAlphabetical);
// ... and so on
the implementation of the pipe function can be found here (not sure why they use a switch case in the implementation).
I hope this is useful and will some day be part of Radash!
If pipe works, what about data supplied last and automatically curry? :) https://ramdajs.com/