哈斯克尔中的合并操作员替代方案



我收到 2 个字符串路径作为参数:inputoutput,我想从input路径读取文件并将其写入output路径。 我想处理有关输入/输出路径的所有 4 种情况。当其中一个为空时,我想给它一个默认值。有没有类似合并操作员的东西?我不想为所有场景重写 do 子句:

场景

func   null _  -> {do clause}
_ null  -> {do clause}
_  _   ->  {do clause}
x  y   ->  {do clause}
let defaultInPath="inPath.txt"
defaultOutPath="outPath.txt"

我想要实现的-do子句:

do 
text<-readFile input??defaultIn
writeFile  output??defaultOut text
return text 

附言我是Haskell的新手,我真的想掌握它。

这里已经提供的其他答案比下面的答案更实用,但如果您对事物的更概念性观点感兴趣,请继续阅读。

首先,Haskell没有空引用,但是如果要对缺失值进行建模,可以使用Maybe。例如,如果要将空字符串视为缺失值,则可以编写如下转换函数:

maybeFromNull :: Foldable t => t a -> Maybe (t a)
maybeFromNull xs = if null xs then Nothing else Just xs

你像这样使用它:

*Q49616294> maybeFromNull "foo"
Just "foo"
*Q49616294> maybeFromNull ""
Nothing

当谈话落在 Haskell 中的零合并运算符上时,这很有趣的原因是,也许有一个与此相对应的幺半群。它被称为First,它从一系列候选项中返回最左边的非Nothing值。

出于稍后可能更清楚的原因,我将使用Data.Semigroup中的那个,所以

import Data.Semigroup

为了获得Maybe上的Monoid行为,你需要将First值包装在一个Option中;例如:

*Q49616294> (Option $ Just $ First 42) <> (Option $ Just $ First 1337)
Option {getOption = Just (First {getFirst = 42})}

当然,这是选择最左边的值的一种相当冗长的方法,但强调零合并"只是"一个幺半群:

*Q49616294> (Option $ Just $ First 42) <> (Option Nothing)
Option {getOption = Just (First {getFirst = 42})}
*Q49616294> (Option Nothing) <> (Option $ Just $ First 1337)
Option {getOption = Just (First {getFirst = 1337})}

由于这对于实际使用来说过于冗长,因此您可以决定编写一个自定义运算符,该运算符Maybe值重新打包为Option First值,应用<>操作,然后将结果从Option First解包回Maybe

(<?>) :: Maybe a -> Maybe a -> Maybe a
mx <?> my =
let ofx = Option $ sequenceA $ First mx
ofy = Option $ sequenceA $ First my
leftmost = ofx <> ofy
in getFirst $ sequenceA $ getOption $ leftmost

虽然你可以把这个运算符写成一个大的表达式,但我选择使用let...in语法来"显示我的作品"。

但是,仍然存在一个问题:

*Q49616294> Just 42 <?> Just 1337
Just 42
*Q49616294> Nothing <?> Nothing
Nothing

只要至少有一个参数是Just值,该操作就会返回Just值,但它可以返回Nothing

如何应用回退值,以便保证在所有情况下都获得值?

您可以利用OptionFoldable,然后仍然折叠<>- 只是这一次,正在使用不同的Monoid实例:

(<!>) :: Maybe a -> a -> a
mx <!> y =
let ofx = Option $ sequenceA $ First mx
fy  = First y
in getFirst $ foldr (<>) fy ofx

这个运算符折叠ofx,使用fy作为初始值。这里,<>属于FirstSemigroup,它无条件返回最左边的值。这里不涉及Option,因为foldr剥离了这一层。但是,由于我们是从右侧折叠的,因此如果初始值fy包含值,则ofx将始终被忽略。

*Q49616294> Just 42 <!> 1337
42
*Q49616294> Nothing <!> 1337
1337

您现在可以按如下方式编写所需的函数:

copyFile :: String -> String -> IO String
copyFile input output = do
text <- readFile $ (maybeFromNull input) <!> defaultInPath
writeFile (maybeFromNull output <!> defaultOutPath) text
return text 

事实证明,在这种情况下,您甚至不需要<?>,但在其他情况下,您可以使用此运算符链接任意数量的潜在值:

*Q49616294> Just 42 <?> Nothing <?> Just 1337 <!> 123
42
*Q49616294> Nothing <?> Nothing <?> Just 1337 <!> 123
1337
*Q49616294> Nothing <?> Nothing <?> Nothing <!> 123
123

这种实现零合并行为的方式不仅不必要地复杂,而且如果它表现不佳以增加伤害,我也不会感到惊讶。

然而,它确实说明了哈斯克尔内置抽象的力量和表现力。

使用Maybe类型构造函数

首先,使用Maybe正确编码"空"字符串。然后,如果参数Nothing,则使用maybe函数返回默认值。

func :: Maybe String -> Maybe String -> IO String
func inFile outFile = do
text <- readFile $ maybe defaultIn id inFile
writeFile (maybe defaultOut id outFile) text
return text

使用Data.Maybe

如果您不介意额外的导入,可以使用fromMaybe d = maybe d id.

import Data.Maybe
func :: Maybe String -> Maybe String -> IO String
func inFile outFile = do
text <- readFile $ fromMaybe defaultIn inFile
writeFile (fromMaybe defaultOut outFile) text
return text

定义自己??

无论哪种方式,您都可以从任一函数定义自己的合并运算符:

?? :: Maybe String -> String -> String
(??) = flip fromMaybe
-- a ?? b = fromMaybe b a
-- a ?? b = maybe b id a

并写入

func inFile outFile = do
text <- readFile (inFile ?? defaultIn)
writeFile (outFile ?? defaultOut) text
return text

使用Maybe

您的四种类型的呼叫将如下所示,假设您还没有 从返回Maybe String值的函数中获取值。

func Nothing Nothing
func (Just "input.txt") Nothing
func Nothing (Just "output.txt")
func (Just "input.txt") (Just "output.txt")

如果你有一个可能提供或可能不提供的值,你绝对应该使用 May 安全灵活地编码它。

但是,如果您真的想替换空字符串或任何其他魔术值,则可以轻松地将if..then..else用作表达式:

func :: String -> IO ()
func input = do 
text <- readFile (if input == "" then defaultIn else input) 
putStrLn text

当然,一旦你切换到 Maybe 并发现自己有一个普通的字符串,你可以使用相同的字符串来调用它:

func :: Maybe String -> IO ()
func input = do
text <- readFile $ fromMaybe "default.txt" input 
putStrLn text
main = do
putStrLn "Enter filename or blank for default:"
file <- getLine
func (if file == "" then Nothing else Just file)

最新更新