函数式编程:基于Task Monad优化路径拼接与无fork生成Stat Task数组
关于函数式编程中Monad组合与Task异步处理的两个疑问
我正在学习函数式编程(FP),终于开始理解Monad的使用了。以下是我的代码:
/* 摘自https://mostly-adequate.gitbooks.io/mostly-adequate-guide/content/ch08.html#asynchronous-tasks */ import fs from 'fs' import { Task, } from './shared/functional.js'; // readDir :: String -> Task Error (Array String) const readDir = path => new Task((reject, result) => { fs.readdir(path, (err, data) => (err ? reject(err) : result(data))); }); const readStat = filename => new Task((reject, result) => { fs.stat(filename, (err, data) => (err ? reject(err) : result(data))); }); console.log(readDir('./shared')) readDir('./shared').fork(console.error,console.log)
通过上述代码,我能获取指定目录下的内容数组。我遇到两个问题:
- 如何避免显式组合时重复调用'./shared'来拼接路径,比如避免如下写法:
const dirList = compose( chain(map(concat('./shared/')))), readDir, );
- 能否在不fork外层Task的情况下,遍历字符串数组并将其转为Task数组?我想获取数组中每个项的stat,不需要得到Task数组,但要避免两次fork。希望调用readDir后直接得到stat数组,而非需要转为Task的字符串数组。我目前尝试的写法是:
const dirList = compose( map(map(compose(readStat, concat('./shared/')))), readDir, );
求各位指点,谢谢!
解决方案
问题1:避免重复拼接路径的问题
你可以通过两种思路解决这个问题,核心都是减少路径的重复书写:
方案1:用闭包固定路径前缀
先封装一个生成路径拼接函数的工具,再针对你的场景绑定基础路径:
// withBasePath :: String -> String -> String const withBasePath = basePath => filename => `${basePath}/${filename}`; // 绑定'./shared'为基础路径,后续直接调用即可 const sharedPath = withBasePath('./shared'); // 组合逻辑中无需重复写路径 const dirList = compose( chain(map(sharedPath)), readDir ); // 使用时仅需传入一次基础路径 dirList('./shared').fork(console.error, console.log);
方案2:改造readDir返回完整路径
直接在readDir内部完成路径拼接,让它返回带完整路径的文件名数组,后续逻辑完全不用处理路径:
// readDirWithFullPath :: String -> Task Error (Array String) const readDirWithFullPath = basePath => new Task((reject, result) => { fs.readdir(basePath, (err, files) => { if (err) reject(err); else result(files.map(file => `${basePath}/${file}`)); }); }); // 使用时直接拿到带完整路径的文件名 readDirWithFullPath('./shared').fork(console.error, console.log);
问题2:获取stat数组无需多次fork的问题
你当前的写法会产生Task Error (Array (Task Error Stat))这种嵌套结构,要解决这个问题,需要用traverse函数将数组中的Task合并为一个包含数组的Task,这样只需一次fork就能拿到最终的stat数组。
自定义实现traverse(针对Task)
如果你的FP工具库没有提供traverse,可以自己实现一个简化版本:
// traverseTask :: (a -> Task e b) -> Task e (Array a) -> Task e (Array b) const traverseTask = taskFn => taskArray => taskArray.chain(arr => { // 遍历数组,依次执行每个Task并收集结果 return arr.reduce((accTask, item) => { return accTask.chain(results => taskFn(item).map(stat => [...results, stat]) ); }, Task.of([])); // 初始值为包含空数组的Task });
结合问题1的方案使用:
// 获取带完整路径的文件名Task const readSharedDir = readDirWithFullPath('./shared'); // 合并所有stat Task为一个返回数组的Task const getSharedDirStats = traverseTask(readStat)(readSharedDir); // 仅一次fork即可拿到所有stat信息 getSharedDirStats.fork(console.error, stats => { console.log('所有文件的stat信息:', stats); });
使用Ramda的traverse(更简洁)
如果项目中使用Ramda,直接用R.traverse就能快速实现:
import R from 'ramda'; const getSharedDirStats = compose( R.traverse(Task.of, readStat), readDirWithFullPath('./shared') ); getSharedDirStats.fork(console.error, console.log);
traverse的核心作用是将“数组包裹Task”的结构,转换为“Task包裹数组”的结构,完美解决了你需要一次fork拿到所有stat的需求。
内容的提问来源于stack exchange,提问作者Ricardo Silva
相关产品推荐
相关产品推荐

