使用“if”创建一个无点函数

14

我正在学Haskell(不,这不是我的作业,我正在为考试学习)。

任务是:

编写一个无参数函数numocc,用于计算给定列表中元素的出现次数。例如:numocc 1 [[1, 2], [2, 3, 2, 1, 1], [3]] = [1, 2, 0]

这是我的代码:

addif :: Eq a => a -> Int -> a -> Int
addif x acc y = if x == y then acc+1 else acc

count :: Eq a => a -> [a] -> Int
count = flip foldl 0 . addif

numocc :: Eq a => a -> [[a]] -> [Int]
numocc = map . count

numocccount是'point-free'的,但它们使用的addif函数不是。

我不知道如何使函数addif成为'point-free'。有没有一种方法可以使if语句成为'point-free'?也许有一种技巧可以不用if实现?


1
你能做一些类似于 let f x y = 1 - ceiling (fromIntegral (x-y) / fromIntegral y) :: Int 这样的数学操作吗?(这也不完全是你所需要的 ;)) - Random Dev
但是,如果我没有错过任何令人讨厌的边界情况,那么1-ceiling(abs $ fromIntegral(x-y)/fromIntegral(max x y)) :: Int就可以解决问题了 - 也许您会想要对此进行推理或运行一些快速检查;)(好吧,我错过了一些负数,所以你需要更多的“abs”...但原则应该是显而易见的...真正的解决方案是微不足道的,留给读者:D - Random Dev
4
通常情况下,您可以使用函数 bool:: Bool->a->a->a 替换 if 语句。其中,当 bool 函数的第一个参数为 False 时,结果为第二个参数;当第一个参数为 True 时,结果为第三个参数。通过这种方式,您可以始终形成一个“正常”的表达式,并使用常规方法将其转化为 point free 形式。 - user2407038
6个回答

11

我会利用一个事实,即您可以使用fromEnum轻松将Bool转换为Int

addif x acc y = acc + fromEnum (x == y)

现在,您可以开始应用通常的技巧来使其变为point-free。
-- Go prefix and use $
addif x acc y = (+) acc $ fromEnum $ (==) x y
-- Swap $ for . when dropping the last argument
addif x acc = (+) acc . fromEnum . (==) x

等等,我不会完全取消点标记的乐趣,尤其是当有工具可以为您完成时。

或者,您可以编写一个类似于以下的函数:

count x = sum . map (fromEnum . (==) x)

这几乎是点无关的,虽然有一些技巧可以让你更接近它,但它们很快变得非常困难:

count = fmap fmap fmap sum map . fmap fmap fmap fromEnum (==)

我认为在这里使用fmap(.)更好看,尽管你可以用(.)替换每个fmap,代码仍然完全相同。实际上,(fmap fmap fmap)将单个参数和双参数函数组合在一起,如果你给它命名为.:,那么可以这样写:

count = (sum .: map) . (fromEnum .: (==))

分解:

> :t fmap fmap fmap sum map
Num a => (a -> b) -> [a] -> b

因此,它需要从b到数字a的函数,一组b,并返回一个a,还不错。

> :t fmap fmap fmap fromEnum (==)
Eq a => a -> a -> Int

这种类型可以写成Eq a => a -> (a -> Int),这是一个重要的事情需要注意。这使得这个函数的返回类型与fmap fmap fmap sum map的输入类型匹配,其中b ~ Int,因此我们可以将它们组合起来得到一个类型为Eq a => a -> [a] -> Int的函数。


8

为什么不呢?

numocc x 
  = map (length . filter (== x))
  = map ((length .) (filter (== x)) )
  = map (((length .) . filter) (== x))
  = map (((length .) . filter) ((==) x))
  = map (((length .) . filter . (==)) x)
  = (map . ((length .) . filter . (==))) x
  = (map . (length .) . filter . (==)) x

然后是微不足道的 eta-缩减。

2
我认为这最符合所描述的任务。 - András Kovács
@AndrásKovács,回溯法... :) 参见这篇最近的Lisp文章,其中有一些等价于map head . filter (not.null.drop 1) . group的复杂方法。 - Will Ness

6

1
为什么不直接使用 fromEnum 而不是 Foreign.Marshal 函数呢?它们的功能相同,而且 fromEnum 已经存在于 Prelude 中了。 - bheklilr
@bheklilr:哦,我没意识到BoolEnum的实例 - 你已经得到了我的点赞 :-) fromBool刚刚在我脑海中闪过,我曾经在过去的某个地方看到过它... - Bergi

1
使用BoolEnum实例,可以构建一个点无形式的替代if语句,可以在更一般的情况下使用:
chk :: Bool -> (a,a) -> a
chk = ([snd,fst]!!) . fromEnum

使用chk,我们可以定义一个不同版本的addIf:
addIf' :: Eq a => a -> a -> Int -> Int
addIf' = curry (flip chk ((+1),id) . uncurry (==))

现在我们可以简单地替换addIf'中的chk:

addIf :: Eq a => a -> a -> Int -> Int
addIf = curry (flip (([snd,fst]!!) . fromEnum) ((+1),id) . uncurry (==))

0
请记住,在Haskell列表推导中,条件语句可以在结果子句或末尾使用。但是,最重要的是,没有if的守卫可以用来过滤结果。我正在使用来自zip的一对对。该对的第二个元素是列表编号。当列表元素与常量(k)进行比较时,它保持不变。 你的结果[1,2,0]不包括列表编号1、2或3,因为从结果列表中的和的位置就可以看出。这里的结果不是将每个列表中的出现次数相加,而是将它们列出来。
nocc k ls = [ z | (y,z) <- zip ls [1..length ls], x <- y, k == x]
nocc 1 [[1, 2], [2, 3, 2, 1, 1], [3]]

[1,2,2] -- 读作 [1,2,0] 或者是列表1中的1,列表2中的2和列表3中的0


0
我认为你正在寻找 Data.Boolbool,它自 4.7.0.0(2014 年 04 月 08 日)以来就存在。
incif :: (Eq a, Enum counter) => a -> a -> counter -> counter
incif = ((bool id succ) .) . (==)

额外的.允许==接受两个参数,然后将表达式传递给bool

由于参数顺序不同,您需要像这样使用incif

(flip . incif)

将其集成到incif中留给读者作为练习。【翻译:这不是一件轻松的事情,而且我还不知道怎么做。;)】


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接