浅谈柯里化之我为什么要用它

我为什么要用柯里化?
在说为什么之前,首先把柯里化解释下

什么是柯里化

百度词条解释为:

在计算机科学中,柯里化是把接受多个参数的函数转换为接受单一参数的函数,并返回接受余下参数且返回结果的新函数的技术。这个技术由 Christopher Strachey以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege发明的。

文字看起来可能有点绕,用代码说明一下:

//经典a+b
const add = (a,b)=>{
    return a+b
}
//手动柯里化
const curry = (fn)=>{
   return (b)=>(a)=>{
       return add(a,b)
   }
}
//原函数
add(1,2)//3
//柯里化处理后
const curryAdd = curry(add)
const a = curryAdd(1)
const b = a(2) //3

从上面的使用情况,可以很直观的看出来,柯里化进行了参数拆分,但是这样做有什么好处呢

脱离业务来讨论技术大多无意义,在前端项目中,接口请求的封装应该都不陌生,我们结合下面的例子一起看下。

//request为axios的request请求,做了一些请求拦截处理
为了方便调用,我们简单封装了post和get两种请求方式

//defaultPrefix 是请求的服务名,比如student-web服务,是请求url的一部分
const defaultPrefix = 'XXXX'
export function postRequest(url,data={},prefix){
    return request({
        url: `${prefix || defaultPrefix}/${url}`,
        method: "POST",
        data
    })
}
export function getRequest(url,data,prefix){
	return request({
        url: `${prefix || defaultPrefix}/${url}`,
        method: "GET",
        params:data
    })
}
//分别调用
postRequest('xxx/xxx',{})
getRequest('xxx/xxx',{})

然后你发现这两种方法重复逻辑很多,区别仅仅在于请求方式和传参,于是做了一个整合处理

//通过method参数区别post/get,再根据请求方式决定数据接受字段是data还是params
export function postRequest(method='post',url,data={},prefix){
    const defaultSet = {
        url: `${prefix || defaultPrefix}/${url}`,
        method
    }
    method.toLowerCase() == 'get' ? defaultSet.params = data : defaultSet.data = data
	return request({
        ...defaultSet
    })
}
//调用
postRequest('post','xxx/xxx',{})

随着业务开展,又有返回格式、请求参数头等的需求增加,最后请求方式修改成如下:

/*
method:请求方式
options:扩展参数,比如headers,responseType等
prefix:请求前缀,默认是student-web
url:请求url部分
data:传参
*/
const defaultPrefix = 'student-web'
export function postRequest(method,url,options,data={},prefix){
    const defaultSet = {
        url: `${prefix || defaultPrefix}/${url}`,
        method,
        ...options
    }
    method.toLowerCase() == 'get' ? defaultSet.params = data : defaultSet.data = data
	return request({
        ...defaultSet
    })
}
//调用一个post请求,该请求返回内容是二进制blob格式
postRequest('post','xxx/xxx/xxx',{
 responseType:"blob"
},{},'')

上面的方法封装完了之后,可以在不同的业务中调用。
但是每次调用都要传好几个参数,而很多参数都是基本固定的,但是也会有少部分请求,参数都不太一样,本着’能懒则懒’的开发原则,柯里化就派上了用场。

lodash工具库里有curry方法可以直接用

import curry from 'lodash/fp/curry'

为了方便理解,我们自己写个简单版的

//core.js
const defaultPrefix = 'XXXX'
const curry = (fn)=>{
    //传入的fn的参数个数
    const fnLen = fn.length
    const innerFun = (...params)=>{
    //如果传入的参数个数少于原函数的参数个数
    //则返回当前函数,并将下一个函数的参数与当前函数的参数合并作为返回函数的参数,继续判断
        if(params.length <fnLen){
            return innerFun(...params,...selfParam)
        }else{
    //如果传入的参数个数等于原函数的参数个数,执行原函数
            return fn(...params)
        }
    }
    return innerFun
}

const coreRequest = curry((method,options,prefix,url,data)=>{
    const dataSet = {}
    method.toLowerCase() == 'get' ? dataSet.params = data : dataSet.data = data
    return request({
        url:prefix+url,
        method,
        ...dataSet,
        ...options
    })
})
//常规json格式请求
const jsonContentType = {
    headers:{
        'Content-Type':'application/json;charset=utf-8'
    }
}
//返回带第一个method参数的函数
const post = coreRequest('post')
const get = coreRequest('get')
//返回请求头部是application/json的post请求函数
const postJson = post(jsonContentType)
//返回请求头部是application/json的get请求函数
const getJson = get(jsonContentType)
//返回带默认服务前缀的post请求
const postApi = postJson(defaultPrefix)
//返回带默认服务前缀的get请求
const getApi = postJson(defaultPrefix)

export{
    getJson,
    postJson,
    postApi,
    getApi
}

业务调用如下:

//service.js
import {postApi} from 'core.js'
const requestFun = postApi('xxxxxx')
//.vue 文件 传入data参数
requestFun({
    name:1
})
或者你也可以直接 postApi('xxxxxx',{name:1})

如果需要修改请求url的默认服务,改为teacher-web,可以重新定义一个函数如下:

//service.js
import {postJson} from 'core.js'
const postDefineApi = postJson('teacher-web')
const requestFun = postDefineApi('xxxxxx')
//.vue 文件 传入data参数
requestFun({
    name:1
})

如果还有别的请求接口需求,可以根据coreRequest做不同的扩展返回不同的函数

上面还有个问题,如果requestFun不传参数(实际情况我们的接口请求,body数据或者param是有可能空的),那么curry方法不会走到最后一步,为了兼容这种情况,可以将curry做如下修改

const curry = (fn)=>{
    //传入的fn的参数个数
    const fnLen = fn.length
    const innerFun = (...params)=>{
        if(params.length <fnLen){
        //如果传入的参数个数少于原函数的参数个数,
        //且后面还有参数则返回当前函数,并将下一个函数的参数与当前函数的参数合并作为返回函数的参数
        //否则执行函数
            return (...selfParam)=>{
                if(!selfParam || selfParam.length == 0){
                    return fn(...params)
                }else{
                    return innerFun(...params,...selfParam)
                }
            }
	}else{
            //如果传入的参数个数等于原函数的参数个数,执行原函数
            return fn(...params)
        }
    }
    return innerFun
}
//调用
//service.js
import {postApi} from 'core.js'
const requestFun = postApi('xxxxxx')
//.vue 文件 传入data参数
requestFun()

这样就兼顾了减少代码量和扩展性,可以少传重复的参数,也可以根据业务需求完成不同的调用
当然,上面的curry方法只是一个简单版,如果调用的参数里面又有函数等更复杂的逻辑,需要再进一步封装。

总结

个人认为柯里化有如下优缺点:

优点

  • 节省重复传参,每一步处理一个参数,更明朗
  • 函数更灵活细腻,每一步都可拆开存储
  • 扩展性高,拆解成的各个函数,根据参数不同,可以组合的可能性就越多

缺点

  • 如果参数多,返回的接受函数较多,看上去有点绕,理解上没有一个函数直观
  • 因为是闭包,可能有内存泄漏风险

当然是否采用柯里化函数,结合实际业务需求来即可。

原文链接:https://juejin.cn/post/7216493408607977509 作者:未知作者

(0)
上一篇 2023年4月3日 下午5:03
下一篇 2023年4月3日 下午5:14

相关推荐

发表回复

登录后才能评论