[Functional Programming] mapReduce over Async operations with first success prediction (fromNode, alt, mapReduce, maybeToAsync)
Let's say we are going to read some files, return the first file which pass the prediction method, this prediction method can be just check whether the file content contains more than 50 chars.
For reading the file, it has tow requirements, first we should have the right to read the file, then read file content, we can use Node.js method:
fs.access
fs.readFile
We won't directly using those methods, we are going to wrap those functions into Async functor:
const {Async, curry} = require('crocks'); const {fromNode} = Async; const access = fromNode(fs.access); const readFile = fromNode(fs.readFile); const accessAsync = curry((mode, path) => access(path, mode) .map(constant(path))); // readFileAsync :: Option -> a -> Async Error b const readFileAsync = curry((option, path) => readFile(path, option));
By using 'fromNode', we are able to conver the Node's method into Async functor.
Here, we also put 'path' to the last params and apply 'curry', this is because we want to partially apply the params in the future.
Now 'accessAsync' & 'readFileAsync' both return 'Async' type, we can compose them:
const {Async, constant, composeK, curry} = require('crocks'); ... // loadTextFile :: String -> Async Error String const loadTextFile = composeK( readTextFile, checkRead );
'loadTextFile' is the only method we want to be exported.
We also create a helper method to fork Async functor:
const fork = a => a.fork( console.log.bind(null, 'rej'), console.log.bind(null, 'res') );
Full Code for funs.js:
const fs = require('fs'); const {Async, constant, composeK, curry} = require('crocks'); const {fromNode} = Async; const access = fromNode(fs.access); const readFile = fromNode(fs.readFile); const accessAsync = curry((mode, path) => access(path, mode) .map(constant(path))); // readFileAsync :: Option -> a -> Async Error b const readFileAsync = curry((option, path) => readFile(path, option)); const checkRead = accessAsync(fs.constants.F_OK); const readTextFile = readFileAsync('utf-8'); // loadTextFile :: String -> Async Error String const loadTextFile = composeK( readTextFile, checkRead ); const fork = a => a.fork( console.log.bind(null, 'rej'), console.log.bind(null, 'res') ); module.exports = { loadTextFile, fork }
Then let's continue to build our main.js file:
Let's say we have an array of filenames:
const data = [ 'text.txt', 'text.big.txt', 'notfound.txt' ];
'text.txt' & 'text.big.txt' are existing files, and only 'text.big.txt' can pass the predicate function:
const isValid = x => x.length > 50;
So with those in mind, let's define what we want to do:
1. We want to map over each filename in the 'data' array, read file content
2. For each content, we want to check against our 'isValid' method.
3. If the checking pass, it's done! output the content
4. If not pass the checking, we continue with next filename, repeat step No.1.
5. If all the filenames have gone though, no matching found, throw error.
6. If the list is empty, throw error.
7. If list is not empty but no matching file, and there is a not found filename, also throw error.
Step1-4 is a the main logic, step 5-7 is just some house keeping, throw some errors...
Step1-4 is prefect case for using 'mapReduce'
'mapReduce' here means, we first mapping over each case, then we do 'reduce' or let's say 'concat'; 'mapReduce' require a "empty" case, since our is Async functor, then the empty case will be a rejected async functor.
const {fork, loadTextFile} = require('./funs.js'); const {Async, curry, safe, mapReduce, maybeToAsync} = require('crocks'); const data = [ 'text.txt', 'notfound.txt', 'text.big.txt', ]; const isValid = x => x.length > 50; const concatAlt = pred => (acc, curr) => acc.alt(curr) .chain(maybeToAsync(new Error('not good!'), safe(pred))) const flow = curry(pred => mapReduce( loadTextFile, //map concatAlt(pred), // reduce Async.Rejected(new Error('list is empty')) //Seed )); fork(flow(isValid, data));
Let's have a look 'concatAlt' in more details:
const concatAlt = pred => (acc, curr) => acc.alt(curr) // If acc async is rejected, then check curr, otherwise continue to next step 'chain' with the value of acc .chain(maybeToAsync(new Error('not good!'), safe(pred))) // Async(Error String) --safe(pred)--> Async(Maybe(Error String)) --maybeToAsync--> Async(Async(Error String)) --chain--> Async(Error String)
'alt': works as fallback option, only has effect when 'acc' is falsy. Which means, if first two files cannot pass 'isValid' checking, but third passing, then we are still good! Also means, if the first one is passing the check, then we are not going to continue with next two files.
Here we are also using natural transform, maybeToAsync, more detail check my another post.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
2017-03-10 [Angular] Export directive functionalities by using 'exportAs'
2017-03-10 [Postgres] Group and Aggregate Data in Postgres
2017-03-10 [Ramda] Create a Query String from an Object using Ramda's toPairs function
2017-03-10 [Ramda] Filter an Array Based on Multiple Predicates with Ramda's allPass Function
2016-03-10 [RxJS] Reactive Programming - Using cached network data with RxJS -- withLatestFrom()