问题基本上是:如何在Haskell中编写一个函数f
,该函数x
和类型参数T
,然后返回一个取决于x
和T
的值y = f x T
,而无需明确将整个表达式的类型归f x T
?(f x T
不是有效的 Haskell,而是占位符伪语法)。
请考虑以下情况。假设我有一个类型类Transform a b
它提供了一个transform :: a -> b
函数。假设我还有一堆instances
Transform
,用于各种类型组合a b
.现在我想将多个transform
函数链接在一起。但是,我希望根据先前构造的链和转换链中的下一个类型来选择Transform
实例。理想情况下,这会给我这样的东西(具有假设的函数source
和migrate
和无效的语法<< >>
"传递类型参数";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
实例,以便它们形成从String
到Int
的两个可能的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
来翻转b
和a
,以便TypeApplications
首先实例化b
。
使用类型应用程序语法扩展。
> :set -XTypeApplications
> transform @_ @Int (transform @_ @Double "9007199254740993")
9007199254740992
> transform @_ @Int (transform @_ @Rational "9007199254740993%1")
9007199254740993
精心选择的输入以使您的"结果与Double
相同"注释的谎言,即使在更正输入中的语法差异之后也是如此。