【问题标题】:How to build a typed variadic function from a container?如何从容器构建类型化的可变参数函数?
【发布时间】:2015-04-07 09:05:39
【问题描述】:

考虑一下可爱的小 HoleyMonoid 库,它可以让您构建类型化的可变参数 printf 类函数,如下所示:

{-# LANGUAGE NoMonomorphismRestriction #-}

import Control.Category
import Data.HoleyMonoid
import Prelude hiding ((.), id)

foo =
    now "hello "
  . later id
  . now ", you are "
  . later show
  . now " years old"

bar = run foo

-- > bar "alice" 42
-- "hello alice, you are 42 years old"

-- > bar 42 "alice"
-- type error

有没有办法检查容器(列表、AST 等)并根据其内容构建这样的函数?

作为一个玩具示例,您可以画出如下图:

import Data.Monoid

adder = go where
  go [] = now (Sum 0)
  go (x:xs)
    | x == 5    = now 100 . go xs
    | otherwise = later id . go xs

-- hypothetical usage
--
-- > :t run adder [1, 3, 5]
-- Num a => Sum a -> Sum a -> Sum a
--
-- > getSum $ run adder [1, 3, 5] 0 1
-- 101

adder 未通过发生检查,但您可以看到我的目标。问题似乎是很难在任何地方保持计算状态,因为 now 100later id 属于不同的类型。

【问题讨论】:

  • 好吧,adder 应该是什么类型?显然您希望类型取决于列表的长度。那将是dependent type。 Haskell 不是 (quite) 依赖类型语言,所以这是不可能的。可行的方法是根据您提供的参数来决定类型,并在与列表不匹配时在运行时崩溃……但这不完全是 Haskell 惯用的。
  • 不适用于常规列表。使用 GADT-y 列表,您可能会实现类似的目标,例如如stackoverflow.com/a/25422805/3234959
  • @leftaroundabout 这可能不是惯用的,但这基本上就是Text.Printf 所做的。除了运行时检查之外,该方法还存在无法处理像show这样的多态函数的问题。
  • 嗯,printf 有一个特殊的应用程序,其中这种特殊的重载机制恰到好处——如果你想在一行中快速打印多个原始(单态)值,可能是不同的类型。如果不是针对不同类型的需求,最好用一个简单的列表参数来实现。
  • 目前在我的实际应用程序中,我将标记为Dynamic 参数的Map 传递给函数,其中标签指示向AST 的哪个节点提供值。这可行,但显然不理想;特别是通过toDyn 手动包装参数是一种痛苦。我不能 100% 确定 printf 风格的路线是否真的更好,但它可能是。

标签: haskell variadic-functions continuations


【解决方案1】:

我将忽略 HoleyMonoid 库。

我们需要自然数:

data Nat = Z | S Nat

将它们提升到类型级别的单例:

data Natty :: Nat -> * where
    Zy :: Natty Z
    Sy :: Natty n -> Natty (S n)

existentials 列表的类型:

data Listy (b :: a -> *) :: [a] -> * where
    Nilly :: Listy b '[]
    Consy :: b x -> Listy b xs -> Listy b (x ': xs)

然后用

type Natties = Listy Natty

我们可以定义

adder :: Natties ns -> Adder ns

在哪里ns :: [Nat]Adder 类型族定义如下:

type family Adder (ns :: [Nat]) :: * where
    Adder '[]       = Int
    Adder (n ': ns) = If (NatEq n (S (S (S (S (S Z)))))) Int (Int -> Adder ns)

即折叠Nats 的列表,在列表中的每个数字前面加上(Int ->),直到遇到5(以Nat 形式)。它实际上应该是这样的

if_then_else_ b x y = if b then x else y

type family Adder (ns :: [Nat]) :: * where
    Adder '[]       = Int
    Adder (n ': ns) = 'if_then_else_ (n '== 'fromInt 5) Int (Int -> Adder ns)

但是 GHC 向我抛出了一些我不想理解的令人毛骨悚然的错误。

NatEq 类型族以显而易见的方式定义:

type family NatEq n m :: Bool where
    NatEq  Z     Z    = True
    NatEq  Z    (S m) = False
    NatEq (S n)  Z    = False
    NatEq (S n) (S m) = NatEq n m

我们需要在价值层面比较Nattys。两个Nattys 相等,如果它们被相同的数字索引(这就是Natty 是单例的原因):

nattyEq :: Natty n -> Natty m -> Booly (NatEq n m)
nattyEq  Zy     Zy    = Truly
nattyEq  Zy    (Sy m) = Falsy
nattyEq (Sy n)  Zy    = Falsy
nattyEq (Sy n) (Sy m) = nattyEq n m

Booly 是另一个单例:

data Booly :: Bool -> * where
    Truly :: Booly True
    Falsy :: Booly False

最后是adder的定义:

adder = go 0 where
    go :: Int -> Natties ns -> Adder ns
    go i  Nilly       = 0
    go i (Consy n ns) = case nattyEq n (Sy (Sy (Sy (Sy (Sy Zy))))) of
        Truly -> i + 100
        Falsy -> \a -> go (i + a) ns

即对所有参数求和,直到遇到5(以Natty 形式),然后添加100。如果列表中没有5,则返回0

测试:

list = Consy Zy $ Consy (Sy Zy) $ Consy (Sy (Sy (Sy (Sy (Sy Zy))))) $ Consy Zy $ Nilly

main = do
    print $ adder (Consy Zy $ Consy (Sy Zy) $ Nilly) 3 9 -- 0
    print $ adder list 6 8                               -- 114
    print $ adder (Consy (Sy (Sy Zy)) list) 1 2 3        -- 106

code

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-05-19
    • 2020-05-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-08-06
    • 2012-09-23
    相关资源
    最近更新 更多