Haskell:接受类型参数并根据该类型返回值的函数?



问题基本上是:如何在Haskell中编写一个函数f,该函数x和类型参数T,然后返回一个取决于xT的值y = f x T,而无需明确将整个表达式的类型归f x T?(f x T不是有效的 Haskell,而是占位符伪语法)。

请考虑以下情况。假设我有一个类型类Transform a b它提供了一个transform :: a -> b函数。假设我还有一堆instancesTransform,用于各种类型组合a b.现在我想将多个transform函数链接在一起。但是,我希望根据先前构造的链和转换链中的下一个类型来选择Transform实例。理想情况下,这会给我这样的东西(具有假设的函数sourcemigrate和无效的语法<< >>"传递类型参数";migrate用作中缀操作):

z = source<<A>> migrate <<B>> ... migrate <<Z>>

在这里,source以某种方式生成了A类型的值,并且每个migrate<<T>>都应该找到一个实例Transform S T并将其附加到链中。

到目前为止,我想出了什么:它实际上(几乎)使用类型归因在 Haskell 中工作。请考虑以下(可编译)示例:

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ExistentialQuantification #-}
-- compiles with: 
-- The Glorious Glasgow Haskell Compilation System, version 8.2.2
-- A typeclass with two type-arguments
class Transform a b where
transform :: a -> b
-- instances of `T` forming a "diamond"
--
--            String
--              /  
--             /    
--            /     
--           /       
--     Double         Rational
--                  /
--                 /
--                /
--               /
--              Int
--
instance Transform String Double where
transform = read
instance Transform String Rational where
transform = read -- turns out to be same as fo `Double`, but pretend it's different
instance Transform Double Int where
transform = round
instance Transform Rational Int where
transform = round -- pretend it's different from `Double`-version

-- A `MigrationPath` to `b` is
-- essentially some data source and
-- a chain of transformations 
-- supplied by typeclass `T`
-- 
-- The `String` here is a dummy for a more
-- complex operation that is roughly `a -> b`
data MigrationPath b = Source b 
| forall a . Modify (MigrationPath a) (a -> b)
-- A function that appends a transformation `T` from `a` to `b`
-- to a `MigrationPath a`
migrate :: Transform a b => MigrationPath a -> MigrationPath b
migrate mpa = Modify mpa transform

-- Build two paths along the left and right side 
-- of the diamond
leftPath :: MigrationPath Int
leftPath = migrate ((migrate ((Source "3.333") :: (MigrationPath String))) :: (MigrationPath Double))
rightPath :: MigrationPath Int
rightPath = migrate((migrate ((Source "10/3") :: (MigrationPath String))) :: (MigrationPath Rational))
main = putStrLn "it compiles, ship it"

在此示例中,我们定义了Transform实例,以便它们形成从StringInt的两个可能的MigrationPath。现在,我们(作为一个人)想要行使我们的自由意志,并迫使编译器在这个转换链中选择左路径或正确路径。

在这种情况下,这甚至是可能的。我们可以通过从类型归属构造约束的"洋葱"来强制编译器创建正确的链:

leftPath :: MigrationPath Int
leftPath = migrate ((migrate ((Source "3.333") :: (MigrationPath String))) :: (MigrationPath Double))

但是,我发现它非常欠佳,原因有两个:

  • AST(migrate ... (Type))Source周围向两侧增长(这是一个小问题,可以使用具有左关联性的中缀运算符进行纠正)。
  • 更严重:如果MigrationPath类型不仅存储目标类型,还存储源类型,那么使用类型归属方法,我们将不得不将链中的每个类型重复两次,这将使整个方法太笨拙而无法使用。

问题:有没有办法构建上述变换链,这样只需要归因"下一个类型",而不是整个"MigrationPath T类型"?

我不是在问:我很清楚,在上面的玩具示例中,定义函数transformStringToInt :: String -> Int等会更容易,然后使用.将它们链接在一起。这不是问题所在。问题是:当我只指定类型时,如何强制编译器生成transformStringToInt对应的表达式。在实际应用程序中,我只想指定类型,并使用一组相当复杂的规则来派生具有正确transform-function 的适当实例。


(可选):只是为了给人一种我正在寻找的印象。这是一个来自Scala的完全类似的例子:

// typeclass providing a transformation from `X` to `Y`
trait Transform[X, Y] {
def transform(x: X): Y
}
// Some data migration path ending with `X`
sealed trait MigrationPath[X] {
def migrate[Y](implicit t: Transform[X, Y]): MigrationPath[Y] = Migrate(this, t)
}
case class Source[X](x: X) extends MigrationPath[X]
case class Migrate[A, X](a: MigrationPath[A], t: Transform[A, X]) extends MigrationPath[X]

// really bad implementation of fractions
case class Q(num: Int, denom: Int) {
def toInt: Int = num / denom
}
// typeclass instances for various type combinations
implicit object TransformStringDouble extends Transform[String, Double] {
def transform(s: String) = s.toDouble
}
implicit object TransformStringQ extends Transform[String, Q] {
def transform(s: String) = Q(s.split("/")(0).toInt, s.split("/")(1).toInt)
}
implicit object TransformDoubleInt extends Transform[Double, Int] {
def transform(d: Double) = d.toInt
}
implicit object TransformQInt extends Transform[Q, Int] {
def transform(q: Q) = q.toInt
}
// constructing migration paths that yield `Int`
val leftPath = Source("3.33").migrate[Double].migrate[Int]
val rightPath = Source("10/3").migrate[Q].migrate[Int]

请注意migrate-方法 只需要"下一个类型",而不是到目前为止构造的整个表达式的类型归属。


相关:我想指出,这个问题并不是"将类型作为参数传递给 Haskell 中的函数?"的精确副本。我的用例有点不同。我也倾向于不同意那里的答案"这是不可能的/你不需要它",因为我实际上确实有一个解决方案,从纯粹的语法角度来看,它相当丑陋。

使用TypeApplications语言扩展,它允许您显式实例化单个类型变量。以下代码似乎具有您想要的风格,并且它进行类型检查:

{-# LANGUAGE ExplicitForAll, FlexibleInstances, MultiParamTypeClasses, TypeApplications #-}
class Transform a b where
transform :: a -> b
instance Transform String Double where
transform = read
instance Transform String Rational where
transform = read
instance Transform Double Int where
transform = round
instance Transform Rational Int where
transform = round
transformTo :: forall b a. Transform a b => a -> b
transformTo = transform
stringToInt1 :: String -> Int
stringToInt1 = transform . transformTo @Double
stringToInt2 :: String -> Int
stringToInt2 = transform . transformTo @Rational

定义transformTo显式使用forall来翻转ba,以便TypeApplications首先实例化b

使用类型应用程序语法扩展。

> :set -XTypeApplications
> transform @_ @Int (transform @_ @Double "9007199254740993")
9007199254740992
> transform @_ @Int (transform @_ @Rational "9007199254740993%1")
9007199254740993

精心选择的输入以使您的"结果与Double相同"注释的谎言,即使在更正输入中的语法差异之后也是如此。

最新更新