守卫语句
在case of中使用守卫语句
1 | checkConditions :: Int -> String |
n | n > 5 && even n定义一个n,且n大于5且n为偶数
1 | let payStatusCondition = |
为何使用|而不是用&&是因为 && 是一个逻辑运算符,它用于连接两个布尔表达式并返回一个布尔值,而守卫 | 后的条件可以包含模式匹配
1 | checkConditions :: Int -> String |
n | n > 5 && even n定义一个n,且n大于5且n为偶数
1 | let payStatusCondition = |
为何使用|而不是用&&是因为 && 是一个逻辑运算符,它用于连接两个布尔表达式并返回一个布尔值,而守卫 | 后的条件可以包含模式匹配
ghcup 是一个用于管理 Haskell 工具链的命令行工具,它可以帮助你轻松安装、更新、切换 Haskell 编译器、GHC(Glasgow Haskell Compiler)版本以及其他相关工具,如 cabal 和 stack。通过 ghcup,你可以方便地管理和配置 Haskell 开发环境,确保不同项目使用不同的工具链版本。
非管理员运行powershell,若ghcup官网下载缓慢,使用镜像网站下载,如下
1 | $env:BOOTSTRAP_HASKELL_YAML = 'https://mirrors.ustc.edu.cn/ghcup/ghcup-metadata/ghcup-0.0.6.yaml' |
执行安装脚本
1 | Set-ExecutionPolicy Bypass -Scope Process -Force;[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072;Invoke-Command -ScriptBlock ([ScriptBlock]::Create((Invoke-WebRequest https://mirrors.ustc.edu.cn/ghcup/sh/bootstrap-haskell.ps1 -UseBasicParsing))) -ArgumentList $true |
安装完成后需要使用ghcup安装和更新软件,如stack等,添加中国镜像
在安装目录下修改配置文件D:\ghcup\config.yml
1 | url-source: |
stack 是一个 Haskell 的构建工具,它简化了 Haskell 项目的构建、管理和部署过程。stack 提供了一些强大的功能,使得 Haskell 开发变得更加高效和容易。其核心功能包括依赖管理、项目构建、版本控制以及与 GHC(Glasgow Haskell Compiler)集成等。
使用ghcup安装/更新stack版本
1 | ghcup install stack |
新建项目
1 | stack new projectname |
新建项目需要下载resolver文件,所以需要代理,clash全局代理对控制台无效,虽然能够curl google.com,但是stack依旧不能走代理,所以添加了代理的环境变量
1 | $env:HTTP_PROXY="http://127.0.0.1:7890" |
如果是开发服务端程序建议在linux环境安装,可在windows的wsl子系统安装,因为访问数据库的一些so文件如果换环境还需要进行匹配。
如果是开发客户端程序那么可以使用windows进行开发,可配合msys2开发,更加方便。
Monad在haskell中是非常常见的操作,一个函数要在不同的Monad中工作就离不开lift。
来自 MonadTrans 类,当使用 Monad Transformer(如 StateT, ReaderT, MaybeT 等)时,操作底层 Monad(例如 IO)需要用 lift。
1 | lift :: Monad m => m a -> t m a |
1 | import Control.Monad.Trans.Class (lift) |
是 fmap 的一个特化,用于 Monad,而不是 Functor。将一个二元函数提升为Monad操作
1 | liftM :: Monad m => (a -> b) -> m a -> m b |
1 | import Control.Monad (liftM) |
(+1)操作本来接收一个Int,提升后可以接收一个IO Int
在 GHC 7.10 后,Applicative 成为了 Monad 的超类,因此可以直接用 fmap,推荐用 fmap 或 <$> 而不是 liftM。
1 | import Control.Monad (liftM) |
liftA 针对的是 Applicative,而 liftM 针对的是 Monad。由于 Monad 是 Applicative 的子类,现在两者的功能重叠,通常用 <$>。
用于将 IO 操作提升到任何支持 MonadIO 的 Monad 中
1 | liftIO :: MonadIO m => IO a -> m a |
1 | import Control.Monad.IO.Class (liftIO) |
yesod框架用于提升handler到另一个Monad中
1 | class (MonadResource m, MonadLogger m) => MonadHandler m where |
fromMaybe描述:当 Maybe 值为 Nothing 时,返回一个默认值。
类型签名:
1 | fromMaybe :: a -> Maybe a -> a |
示例
1 | import Data.Maybe (fromMaybe) |
描述:解析字符串为指定类型(如 Int),如果解析失败则返回 Nothing。
类型签名:
1 | readMaybe :: Read a => String -> Maybe a |
示例
1 | import Text.Read (readMaybe) |
描述:对列表中的每个元素进行转换,并过滤掉转换失败的元素。
类型签名:
1 | mapMaybe :: (a -> Maybe b) -> [a] -> [b] |
示例
1 | import Data.Maybe (mapMaybe) |
描述:将列表中所有 Just 的值提取出来,忽略 Nothing。
类型签名:
1 | catMaybes :: [Maybe a] -> [a] |
示例
1 | import Data.Maybe (catMaybes) |
描述:将 Maybe 转换为另一个值。第一个参数为 Nothing 的转换值,第二个参数为 Just 的转换函数。
类型签名:
1 | maybe :: b -> (a -> b) -> Maybe a -> b |
示例
1 | let result = maybe 0 (*2) (Just 5) -- 结果为 10 |
描述:将一个普通值转换为 Maybe 值:如果条件满足则返回 Just,否则返回 Nothing。
类型签名:
1 | hoistMaybe :: Bool -> a -> Maybe a |
示例
1 | hoistMaybe :: Bool -> a -> Maybe a |
先来看一个例子,在后端收到一个请求后需要解析该请求的请求头获取token,获取到token后需要从redis中查询该token的用户数据。现在有个现成的方法如下
1 | -- 获取请求头的token |
那么现在获取用户信息json的方法如下
1 | getUser :: Handler (Maybe User) |
可以发现,因为每一步都存在Nothing的可能性,所以每一步都需要判断,那么有没有什么简写的方式呢?答案是,有的。
Monad的签名如下
1 | class Applicative m => Monad m where |
>>=用于将m a转为m b,其中(a -> mb)是转换的方法。形象的理解就是m a可以当成包含元素a的集合,m b可以当成包含元素b的集合,a -> m b就是将元素a转为包含元素b的集合,用js的命令式写法就是
1 | function >>=(list: a[], fun: (item: a) => b[]): b[]{ |
当然,m不等于数组,这里只是为了形象。
在实现的内容里,我们除了可以使用a -> m b来处理m a中的元素,我们还能添加额外的操作,而这些额外的操作在do和return语法糖中是看不到的。
下面是一个实现隐式操作的Monad
1 | data MyContainer a = MyContainer a |
知道了Monad可以实现隐式操作之后,我们就可以将Nothing的判断放到Monad的隐式操作中
1 | data MaybeT a = MaybeT |
下面是mtl包下的MaybeT的Monad实现
1 | newtype MaybeT m a = MaybeT { |
对比上面的设计,这种设计增加了一个Monad参数m。这么做的原因是因为MaybeT 的主要目的是作为 Monad Transformer,使 Maybe Monad 能够与其他 Monad(如 IO、List 等)堆叠。通过定义为 MaybeT m a = m (Maybe a),它可以包裹任何 Monad 上下文 m,使其兼容多种 Monad,而不是仅限于单一的 Maybe 值。
使用MaybeT实现隐式判断nothing
1 | getUser :: Handler (Maybe User) |
总结,Maybe的Monad实现使得在Maybe的上下文中能够自动过滤Nothing,但在m (Maybe a)是在m的上下文,为了使其能实现Maybe上下文相同的效果,通过MaybeT包装m (Maybe a),并为其实现过滤Nothing的逻辑。
MonadPlus 是 Haskell 中的一个类型类,它结合了 Monad 和 Alternative 的特性,提供了额外的处理组合逻辑和失败的能力。
它在处理带有失败、非确定性和多重选择的计算时非常有用,常见的实例包括 Maybe、[] (列表) 和 Either。
MonadPlus 定义了两个主要操作:
Maybe 是 MonadPlus 的一个实例,可以在链式操作中表示失败。例如,多个计算中只要有一个失败(即 Nothing),就直接返回失败。
1 | import Control.Monad (mplus) |
如果第一个 findValue 1 成功,那么结果为 Just “One”;如果失败(返回 Nothing),则继续尝试第二个计算。
列表([])是 MonadPlus 的另一个实例,可以用于处理多个选项的情况。比如,生成所有满足特定条件的组合。
1 | import Control.Monad (guard) |
列表类型表示一种“非确定性”计算——一个列表可以包含多个可能的值。
这个例子中,通过mplus生成所有可能的分支,guard函数再对部分不符合条件的分支返回mzero。
mplus并不是直接使用,而是通过>>=使用了concatMap达到mplus的效果。
1 | filteredPairs :: [(Int, Int)] |
至于为什么能变成四个组合,可以不用语法糖do来理解
1 | filteredPairs :: [(Int, Int)] |
1 | instance MonadPlus [] where |
guard的实现依赖于MonadPlus
1 | guard :: (MonadPlus m) => Bool -> m () |
可以使用 guard(来自 Control.Monad)在 MonadPlus 上添加条件限制。例如,过滤出满足条件的值。
1 | import Control.Monad (guard) |
在需要依次尝试多种方法来解决问题时,mplus 可以帮助实现这种链式的短路操作。
1 | safeDivide :: Int -> Int -> Maybe Int |
MonadPlus 的应用主要体现在处理非确定性、多重选择、错误短路等场景中,适用于需要组合多种可能性的计算。
1 | {-# LANGUAGE MultiParamTypeClasses #-} |
对于一个需要环境变量r的函数,既可以使用MonadReader约束从而运行在不同的上下文中,也可以使用ReaderT作为签名。
ReaderT的本质就是一个r -> m a的函数,m a也是一个函数,通过闭包的方式访问r。
1 | class MonadTrans t where |
1 | class DictEnum a where |
我想对任意实现了DictEnum的类型自动实现ToJSON,因为他们的json结构是一样的。
如果按照上述方式写,会有问题。因为上述的ToJSON实现针对任意类型,那么没有实现ToJSON的类型会自动使用这个实现,这就导致了不需要实现DictEnum的类型会报错说没有实现DictEnum。
所以,修改了代码改成如下方式
1 | class DictEnum a where |
在使用的地方实现ToJSON
1 | instance ToJSON ProficiencyEnum where |
1 | newtype ServeFor site param a = ServeFor (ReaderT (ServiceContext site param) (HandlerFor site) a) |
该类型是一个多参数类型,现在需要对其写个liftS,使得ServeFor可以提升到其他Monad
1 | class Monad m => MonadServe site param m where |
这时候两个site和两个param并不相等,所以在DbServeFor中调用liftS并无法将ServeFor提升到DbServeFor。
所以需要添加约束使得前后的site和param相等,这时候有多种方式。
1 | liftSDBS :: forall site param a. ServeFor site param a -> DbServeFor site param a |
这种方式比较冗余,为了添加约束需要另外定义一个函数
1 | test :: DbServeFor site param a |
在定义class的时候就添加约束
1 | class Monad m => MonadServe m where |
type ServeSite m和type ServeParam m不仅仅是为了转移了class的参数,还说明了这两个属性从m推导。
1 | -- site param m各自独立,没有关系 |
1 | handleDBException :: SomeException -> HandlerFor site (Either DatabaseException a) |
上述函数并没有使用到HandlerFor这个类型相关的函数,完全可以用Monad约束代替。
1 | handleDBException :: Monad m => SomeException -> m (Either DatabaseException a) |
在构建newtype的时候也是一样,例如构建一个ReaderT
1 | newtype ServeFor site param a = ServeFor (ReaderT (ServiceContext param) (HandlerFor site) a) |
也可以修改如下
1 | newtype Monad m => ServeFor param m a = ServeFor (ReaderT (ServiceContext param) m a) |