Functional programming: Retrieve the initial truthy output from executing an array of diverse functions using a particular parameter

As I delve into the world of functional programming in TypeScript, I find myself contemplating the most idiomatic way to achieve a specific task using libraries like ramda, remeda, or lodash-fp. My goal is to apply a series of functions to a particular dataset and return the first truthy result. Ideally, I would like to avoid running the remaining functions once a truthy result is found, especially considering that some of the later functions can be quite computationally expensive. Here's an example of how this can be done in regular ES6:

const firstTruthy = (functions, data) => {
    let result = null
    for (let i = 0; i < functions.length; i++) {
        res = functions[i](data)
        if (res) {
            result = res
            break
        }
    }
    return result
}
const functions = [
  (input) => input % 3 === 0 ? 'multiple of 3' : false,
  (input) => input * 2 === 8 ? 'times 2 equals 8' : false,
  (input) => input + 2 === 10 ? 'two less than 10' : false
]
firstTruthy(functions, 3) // 'multiple of 3'
firstTruthy(functions, 4) // 'times 2 equals 8'
firstTruthy(functions, 8) // 'two less than 10'
firstTruthy(functions, 10) // null

Although this function gets the job done, I wonder if there is a pre-existing function in any of these libraries that could achieve the same result, or if I could chain together some of their existing functions to accomplish this. My main goal is to grasp functional programming concepts and seek advice on the most idiomatic approach to solving this problem.

Answer №1

Although Ramda's anyPass function shares a similar concept, it simply returns a boolean value if any of the functions return true. Ramda, of which I am an author (disclaimer), does not currently have this specific function. If you believe it should be included in Ramda, please don't hesitate to bring it to our attention by creating a new issue or submitting a pull request. While we can't guarantee its acceptance, we can promise a fair consideration.

Scott Christopher showcased what is arguably the most elegant Ramda solution.

One suggestion that hasn't been proposed yet is a simple recursive approach (though Scott Christopher's lazyReduce has some similarities). Here's one method:

const firstTruthy = ([fn, ...fns], ...args) =>
  fn == undefined 
    ? null
    : fn (...args) || firstTruthy (fns, ...args)

const functions = [
  (input) => input % 3 === 0 ? 'multiple of 3' : false,
  (input) => input * 2 === 8 ? 'times 2 equals 8' : false,
  (input) => input + 2 === 10 ? 'two less than 10' : false
]

console .log (firstTruthy (functions, 3)) // 'multiple of 3'
console .log (firstTruthy (functions, 4)) // 'times 2 equals 8'
console .log (firstTruthy (functions, 8)) // 'two less than 10'
console .log (firstTruthy (functions, 10)) // null

I personally would opt to curry the function, either using Ramda's curry or manually like so:

const firstTruthy = ([fn, ...fns]) => (...args) =>
  fn == undefined 
    ? null
    : fn (...args) || firstTruthy (fns) (...args)

// ...

const foo = firstTruthy (functions);

[3, 4, 8, 10] .map (foo) //=> ["multiple of 3", "times 2 equals 8", "two less than 10", null]

Alternatively, I might use this version:

const firstTruthy = (fns, ...args) => fns.reduce((a, f) => a || f(...args), null)

(or a curried version of it) which is quite similar to Matt Terski's solution, with the distinction that the functions can accept multiple arguments here. It's worth noting a subtle difference - in the original and the aforementioned solution, the absence of a match results in null. Here, it is the result of the last function if none of the others returned a truthy value. This probably is a minor issue, easily remedied by adding a || null statement at the end.

Answer №2

You can apply Array#some method and implement a short circuit based on a truthy value.

const
    findFirstTruthy = (functions, data) => {
        let result;
        functions.some(fn => result = fn(data));
        return result || null;
    },
    functions = [
        input => input % 3 === 0 ? 'multiple of 3' : false,
        input => input * 2 === 8 ? 'times 2 equals 8' : false,
        input => input + 2 === 10 ? 'two less than 10' : false
    ];

console.log(findFirstTruthy(functions, 3)); // 'multiple of 3'
console.log(findFirstTruthy(functions, 4)); // 'times 2 equals 8'
console.log(findFirstTruthy(functions, 8)); // 'two less than 10'
console.log(findFirstTruthy(functions, 10)); // null

Answer №3

When working with Ramda, there is a clever way to optimize the performance of certain functions like `R.reduce`. By utilizing the `R.reduced` function, you can signal for the iteration to stop traversing through the list. This not only prevents unnecessary function applications but also halts further iterations, which can be especially handy when dealing with potentially large lists.

const firstTruthy = (fns, value) =>
  R.reduce((acc, nextFn) => {
    const nextVal = nextFn(value)
    return nextVal ? R.reduced(nextVal) : acc
  }, null, fns)

const functions = [
  (input) => input % 3 === 0 ? 'multiple of 3' : false,
  (input) => input * 2 === 8 ? 'times 2 equals 8' : false,
  (input) => input + 2 === 10 ? 'two less than 10' : false
]

console.log(
  firstTruthy(functions, 3), // 'multiple of 3'
  firstTruthy(functions, 4), // 'times 2 equals 8'
  firstTruthy(functions, 8), // 'two less than 10'
  firstTruthy(functions, 10) // null
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.0/ramda.min.js"></script>

Another approach is to implement a "lazy" version of `reduce`, where the iteration only continues if the function passed as the accumulated value is applied. This allows for recursive iteration control within the reducing function, enabling you to short-circuit by choosing not to evaluate the remaining values in the list.

const lazyReduce = (fn, emptyVal, list) =>
  list.length > 0
    ? fn(list[0], () => lazyReduce(fn, emptyVal, list.slice(1)))
    : emptyVal

const firstTruthy = (fns, value) =>
  lazyReduce((nextFn, rest) => nextFn(value) || rest(), null, fns)

const functions = [
  (input) => input % 3 === 0 ? 'multiple of 3' : false,
  (input) => input * 2 === 8 ? 'times 2 equals 8' : false,
  (input) => input + 2 === 10 ? 'two less than 10' : false
]

console.log(
  firstTruthy(functions, 3), // 'multiple of 3'
  firstTruthy(functions, 4), // 'times 2 equals 8'
  firstTruthy(functions, 8), // 'two less than 10'
  firstTruthy(functions, 10) // null
)

Answer №4

Whenever I need to condense an array of items into a single value, my go-to is the reduce() method. It seems like it could come in handy in this scenario.

Create a reducer that will execute functions in the array until it returns a truthy value.

const functions = [
    (input) => (input % 3 === 0 ? 'multiple of 3' : false),
    (input) => (input * 2 === 8 ? 'times 2 equals 8' : false),
    (input) => (input + 2 === 10 ? 'two less than 10' : false),
];

const firstTruthy = (functions, x) =>
    functions.reduce(
        (accumulator, currentFunction) => accumulator || currentFunction(x),
        false
    );

[3, 4, 8, 10].map(x => console.log(firstTruthy(functions, x)))

I included a console.log statement to enhance the visibility of the result.

Answer №5

Utilizing Ramda library, one approach could be centered around R.cond. This function accepts a list of pairs [predicate, transformer], where if the predicate(data) evaluates to true, it will return transformer(data). In this scenario, since the transformer and predicate are identical, you can utilize R.map to repeat them:

const { curry, cond, map, repeat, __ } = R

const firstTruthy = curry((fns, val) => cond(map(repeat(__, 2), fns))(val) ?? null)

const functions = [
  (input) => input % 3 === 0 ? 'multiple of 3' : false,
  (input) => input * 2 === 8 ? 'times 2 equals 8' : false,
  (input) => input + 2 === 10 ? 'two less than 10' : false
]

console.log(firstTruthy(functions, 3)) // 'multiple of 3'
console.log(firstTruthy(functions, 4)) // 'times 2 equals 8'
console.log(firstTruthy(functions, 8)) // 'two less than 10'
console.log(firstTruthy(functions, 10)) // null
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js" integrity="sha512-rZHvUXcc1zWKsxm7rJ8lVQuIr1oOmm7cShlvpV0gWf0RvbcJN6x96al/Rp2L2BI4a4ZkT2/YfVe/8YvB2UHzQw==" crossorigin="anonymous"></script>

An alternative approach is to construct your array of functions (pairs) directly for R.cond by separating the predicate and the return value. Since cond requires a function as the transformer, wrap the return value with R.always:

const { curry, cond, always } = R

const firstTruthy = curry((pairs, val) => cond(pairs)(val) ?? null)

const pairs = [
  [input => input % 3 === 0, always('multiple of 3')],
  [input => input * 2 === 8, always('times 2 equals 8')],
  [input => input + 2 === 10, always('two less than 10')]
]

console.log(firstTruthy(pairs, 3)) // 'multiple of 3'
console.log(firstTruthy(pairs, 4)) // 'times 2 equals 8'
console.log(firstTruthy(pairs, 8)) // 'two less than 10'
console.log(firstTruthy(pairs, 10)) // null
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js" integrity="sha512-rZHvUXcc1zWKsxm7rJ8lVQuIr1oOmm7cShlvpV0gWf0RvbcJN6x96al/Rp2L2BI4a4ZkT2/YfVe/8YvB2UHzQw==" crossorigin="anonymous"></script>

Another method involves using Array.find() to locate a function that provides a truthy result (the string). If a matching function is found, employing optional chaining, it is executed again with the initial data to produce the actual output, or else null is returned:

const firstTruthy = (fns, val) => fns.find(fn => fn(val))?.(val) ?? null

const functions = [
  (input) => input % 3 === 0 ? 'multiple of 3' : false,
  (input) => input * 2 === 8 ? 'times 2 equals 8' : false,
  (input) => input + 2 === 10 ? 'two less than 10' : false
]

console.log(firstTruthy(functions, 3)) // 'multiple of 3'
console.log(firstTruthy(functions, 4)) // 'times 2 equals 8'
console.log(firstTruthy(functions, 8)) // 'two less than 10'
console.log(firstTruthy(functions, 10)) // null

However, your existing code accomplishes the desired outcome effectively and is straightforward to understand, terminating early once a result is obtained.

The only minor modifications I would suggest are replacing the for loop with a for...of loop and opting for an early return instead of breaking when a result is identified:

const firstTruthy = (functions, data) => {
  for (const fn of functions) {
    const result = fn(data)
    
    if (result) return result
  }
  
  return null
}

const functions = [
  (input) => input % 3 === 0 ? 'multiple of 3' : false,
  (input) => input * 2 === 8 ? 'times 2 equals 8' : false,
  (input) => input + 2 === 10 ? 'two less than 10' : false
]

console.log(firstTruthy(functions, 3)) // 'multiple of 3'
console.log(firstTruthy(functions, 4)) // 'times 2 equals 8'
console.log(firstTruthy(functions, 8)) // 'two less than 10'
console.log(firstTruthy(functions, 10)) // null

Answer №6

This reminds me of a discussion on Stack Overflow about whether there is a variadic version of `either (R.either)`. If you're interested, you can check out Is there a Variadic Version of either (R.either)?

When it comes to the terminology, I personally find it clearer to refer to `firstMatch` rather than `firstTruthy`. A `firstMatch` essentially functions like an `either` function, particularly in the context of a variadic either function.

For example, you can define a `firstMatch` function using Ramda like this:


const either = (...fns) => (...values) => {
  const [left = R.identity, right = R.identity, ...rest] = fns;
  
  return R.either(left, right)(...values) || (
    rest.length ? either(...rest)(...values) : null
  );
};

const firstMatch = either(
  (i) => i % 3 === 0 && 'multiple of 3',
  (i) => i * 2 === 8 && 'times 2 equals 8',
  (i) => i + 2 === 10 && 'two less than 10',
)

console.log(
  firstMatch(8),
);

Make sure to include the Ramda library by adding this script tag:

<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.js" integrity="sha512-3sdB9mAxNh2MIo6YkY05uY1qjkywAlDfCf5u1cSotv6k9CZUSyHVf4BJSpTYgla+YHLaHG8LUpqV7MHctlYzlw==" crossorigin="anonymous"></script>

Answer №7

Implement Array.prototype.find in your code for a more efficient solution:

const numbers = [2, 5, 9, 12];
const firstMatch = numbers.find(num => conditions.find(cond => cond(num)))

Essentially, the find method locates the first element in the array that satisfies the condition specified in the callback function. It terminates the search as soon as a matching element is identified.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Performing asynchronous ajax calls with jQuery

Here is some code I have that involves a list and making an ajax call for each element in the list: util.testMethod = function(list) { var map = new Map(); list.forEach(function(data) { $.ajax({ ...

Struggling with extracting an array of objects from a JSON file and accessing individual attributes within each object?

As a newcomer to Typescript, I am eager to work with JSON and access its objects and attributes. However, I am encountering difficulties in achieving this. I have attempted using functions like 'for of' and 'for in', but unfortunately, ...

Prevent selection based on function in Angular

I'm attempting to prevent certain options from being selected based on a specific method. For instance, let's say I have four options: A B C D In my method (let's use "x" as an example): if(name == A) { disable the selection for option A. ...

Clicking will cause my background content to blur

Is there a way to implement a menu button that, when clicked, reveals a menu with blurred content in the background? And when clicked again, the content returns to normal? Here's the current HTML structure: <div class="menu"> <div class=" ...

Encountering an Uncaught TypeError in Reactjs: The property 'groupsData' of null is not readable

While working on a ReactJs component, I encountered an issue with two basic ajax calls to different APIs. Even though I am sure that the URLs are functioning and returning data, I keep getting the following error message: Uncaught TypeError: Cannot read p ...

Error message: JavaScript is unable to save data to an array retrieved from Ajax, resulting in

I am facing an issue with retrieving continuous data from the database using AJAX and storing it in a JavaScript variable. Despite my efforts, I am unable to populate an array with the retrieved values as they always end up being undefined. Below are the s ...

Using jQuery, retrieve JSON data from a different domain URL, even if the JSON data is not well-structured

Currently, I am utilizing jQuery's ajax function to access a URL from a different domain which returns JSON data. During my testing phase, I've encountered an issue where the presence of multiple '&quot' strings within the JSON valu ...

What is the best way to instruct firebase-admin to halt during test execution?

Running process.exit() or --exit in my mocha tests doesn't feel right. I'm currently working on code that utilizes firebase-admin and while attempting to run mocha tests, wtfnode showed me: wtfnode node_modules/.bin/_mocha --compilers coffee:co ...

Issue with grid breakpoints for medium-sized columns and grid rows not functioning as expected

I'm working on building a grid layout from scratch in Vue.js and have created my own component for it. In my gridCol.vue component, I have defined several props that are passed in App.vue. I'm struggling to set up breakpoints for different screen ...

What is the best way to access props from a different file in a React application?

I am facing a challenge with my two files: EnhancedTableHead and OrderDialog. I need to access the props data from EnhancedTabledHead in my OrderDialog file. Is there a way to achieve this? Here is how my files are structured: //OrderDialog.jsx import ...

Can we limit the return type of arrow function parameters in TypeScript?

Within my typescript code, there is a function that takes in two parameters: a configuration object and a function: function executeMaybe<Input, Output> ( config: { percent: number }, fn: (i: Input) => Output ): (i: Input) => Output | &apos ...

Attempting to scroll through a webpage and extract data using Python and Selenium in a continuous manner

Recently, I posed a question (you can find it here: Python Web Scraping (Beautiful Soup, Selenium and PhantomJS): Only scraping part of full page) that shed light on an issue I encountered while attempting to scrape all the data from a webpage that updates ...

Error in React Typescript Order Form when recalculating on change

When creating an order form with React TypeScript, users can input the quantity, unit price, and select whether the item is taxable. In this simplified example, only 1 or 2 items can be added, but in the final version, users will be able to add 10-15 item ...

Adjusting the contenteditable feature to place the caret on a specific child node

I am experiencing some challenges when attempting to position the cursor after an <i> tag within a contenteditable element. Currently, this is what I have: <p contenteditable="true"><i>H</i><i>e</i><i>l</i> ...

Error: Unable to locate specified column in Angular Material table

I don't understand why I am encountering this error in my code: ERROR Error: Could not find column with id "continent". I thought I had added the display column part correctly, so I'm unsure why this error is happening. <div class="exa ...

Is there a method I can use to transform this PHP script so that I can incorporate it in .JS with Ajax?

I have a JavaScript file hosted on domain1.com, but in order for it to function properly, I need to include some PHP code at the beginning. This is necessary to bypass certain restrictions on Safari for my script by creating a session. The PHP code establi ...

Having trouble reaching a public method within an object passed to the @Input field of an Angular component

My configurator object declaration is as follows. export class Config { constructor(public index: number, public junk: string[] = []) { } public count() : number { return this.junk.length; } } After declaring it, I pass it into the input decorated fi ...

How can we effectively divide NGXS state into manageable sections while still allowing them to interact seamlessly with one another?

Background of the inquiry: I am in the process of developing a web assistant for the popular party game Mafia, and my objective is to store each individual game using NGXS. The GitLab repository for this project can be found here. The game includes the f ...

Difficulty encountered when attempting to utilize keyup functionality on input-groups that are added dynamically

I've exhausted all available questions on this topic and attempted every solution, but my keyup function remains unresponsive. $(document).ready(function() { $(document).on('keyup', '.pollOption', function() { var empty = ...

The Angular component is failing to display the template

After following a tutorial on UI-Router () I have implemented the following states in my Angular application: angular .module('appRoutes', ["ui.router"]) .config(['$stateProvider', '$urlRouterProvider', function($sta ...