AndrogenAgonist
AndrogenAgonist

Reputation: 284

Is there a better way to compose this in a point-free way?

I come across this use-case at work all the time and I feel like there must be a way to compose fullName in a point-free way without defining cat as a parameter:

    const cats = [
      { name: 'Bob', lastName: 'Ross' },
      { name: 'Frank', lastName: 'Langella' },
    ];
    
    // this bugs me
    const fullName = cat => add(
      prop('name', cat),
      prop('lastName', cat)
    );
    
    const isEqual = curry((a, b) => a === b);
    const isBobRoss = compose(isEqual('BobRoss'), fullName);

edit: some of the helpers above in case it helps understand the challenge

/**
 *  compose :: ((a -> b), (b -> c),  ..., (y -> z)) -> a -> z
 */
const compose = (...fns) => (...args) => 
  fns.reduceRight((res, fn) => [fn.call(null, ...res)], args)[0];

/**
 *  curry :: ((a, b, ...) -> c) -> a -> b -> ... -> c
 */
function curry(fn) {
  const arity = fn.length;

  return function $curry(...args) {
    if (args.length < arity) {
      return $curry.bind(null, ...args);
    }

    return fn.call(null, ...args);
  };

/**
 *  add :: a -> b -> a + b
 */
const add = curry((x, y) =>  x + y)

/**
 *  prop :: String -> Object -> a
 */
const prop = curry((p, obj) => obj[p])
}

Upvotes: 2

Views: 122

Answers (2)

user1713450
user1713450

Reputation: 1429

(Initially I decline to commit to a specific functional programming library for JS/TS and work in general concepts)

Notice that your curred prop function takes a key and returns a reader monad. This will be useful.

Assuming type Obj = {name:string,lastName:string}, then your curried prop fn is (key:'name'|'lastName') => Reader<Obj,string>

You can use a sequence type function to combine two reader monads into a single one, as such:

const getNameParts = sequence(prop('name'), prop('lastName')) // Reader<Obj, [string,string]>

Then you can map the [string,string] to be a single string like in your add function

const add = as => as.reduce((acc, item) => acc + item)

So if you can lift add into your reader monad's computational context (here using a proposed `map: ((a:A)=>B)=>(fa:F<A>)=>F<B>), then compose these operations:

const buildNameFromObject = compose(getNameParts, map(add)) // Reader<Obj, string>

There we have it.

const personsName = buildNameFromObject(someObject) // string

fp-ts is a library that provides everything I just mentioned, and using that library (with a few function name changes to align with fp-ts's vocabulary),

import { reader, map } from 'fp-ts/lib/Reader'
import { sequenceT } from 'fp-ts/lib/Apply'
import { pipe } from 'fp-ts/lib/pipeable'

const getNameParts = sequenceT(reader)(prop('name'), prop('lastName'))
const buildNameFromObject = pipe(getNameParts, map(add)) // Reader<Obj, string>, which is a fancy way of writing (o:Obj)=>string
buildNameFromObject({name:'Foo', lastName: 'Bar'}) // 'FooBar'

Your "fullName" function (buildNameFromObject) is now point free.

Upvotes: 2

Bergi
Bergi

Reputation: 664620

This is no exactly function composition, but sure you can write a helper function for it. Ramda does know it as converge, here's a simplified (unary) version of it:

const converge = (fn, wraps) => arg => fn(...wraps.map(wrap => wrap(arg)));

cost fullName = converge(add, [prop('name'), prop('lastName')]);

Upvotes: 2

Related Questions