Haskell类型系统入门:「Haskell中的非类型变量参数在约束条件中」错误。

3

为了尝试一下函数式编程,我正在学习Haskell,并在类型系统方面遇到了一些问题。

运行以下代码会产生正确的输出(例如,在角度theta处生成绕半径为R的圆柱体上的圆的坐标):

coilGeneration_AngleTest housingRadius coilWidth coilDepth numEle zoffset centralAngle
     = [ (x',y',z)
       | theta <- [0,2*pi/(numEle-1)..2*pi]
       , let x = housingRadius * cos(coilWidth*cos(theta)/housingRadius)
       , let y = housingRadius * sin(coilWidth*cos(theta)/housingRadius)
       , let z = coilDepth * sin(theta)+zoffset
       , let x' = x * cos(centralAngle) - y * sin(centralAngle)
       , let y' = x * sin(centralAngle) + y * cos(centralAngle)
       ]

样例coilGeneration_AngleTest函数输出

然而,尝试将其泛化为一个函数,通过运行来生成一个在极向和z向具有不同重叠的任意NxM数组的圆:

coilArrayGeneration_Test r nE width depth n m mu gam
     = [ (x',y',z',i,j)
       | theta <- [0,2*pi/(nE-1)..2*pi]
       , i <- [1..n]
       , j <- [1..m]
       , let a = width/2
       , let b = depth/2
       , let x = r * cos(a*cos(theta)/r)
       , let y = r * sin(a*cos(theta)/r)
       , let z = b * sin(theta)
       , let phi = (2*i-1-n)((a-mu)/r)
       , let zo = (2*j-1-m)(b-gam)
       , let x' = x * cos(phi) - y * sin(phi)
       , let y' = x * sin(phi) + y * cos(phi)
       , let z' = z + zo
       ]

出现以下错误:

Build profile: -w ghc-9.2.5 -O1
In order, the following will be built (use -v for more details):
 - Haskell-0.1.0.0 (exe:Haskell) (file app/Main.hs changed)
Preprocessing executable 'Haskell' for Haskell-0.1.0.0..
Building executable 'Haskell' for Haskell-0.1.0.0..
[1 of 1] Compiling Main             ( app/Main.hs, /Users/zack/Desktop/Udemy/Haskell/dist-newstyle/build/aarch64-osx/ghc-9.2.5/Haskell-0.1.0.0/x/Haskell/build/Haskell/Haskell-tmp/Main.o )

app/Main.hs:66:1: error:
    • Non type-variable argument in the constraint: Num (c -> c)
      (Use FlexibleContexts to permit this)
    • When checking the inferred type
        coilArrayGeneration_Test :: forall {c}.
                                    (Floating c, Num (c -> c), Enum c, Enum (c -> c)) =>
                                    c
                                    -> c
                                    -> c
                                    -> c
                                    -> (c -> c)
                                    -> (c -> c)
                                    -> c
                                    -> c
                                    -> [(c, c, c, c -> c, c -> c)]
   |
66 | coilArrayGeneration_Test r nE width depth n m mu gam = [(x',y',z',i,j)|theta <- [0,2*pi/(nE-1)..2*pi],....

失败输出

在谷歌上搜索一段时间后,看起来我的函数有一个不正确的类型被编译器暗示了,但我不幸的是不太了解 Haskell 类型定义的概念,无法修复它。我试图按照我理解的方式定义类型,即:

  • r -> 双精度浮点数

  • nE -> 整数

  • width -> 双精度浮点数

  • depth -> 双精度浮点数

  • n -> 整数

  • m -> 整数

  • mu -> 双精度浮点数

  • gam -> 双精度浮点数

  • x' -> 双精度浮点数

  • y' -> 双精度浮点数

  • z' -> 双精度浮点数

  • I -> 整数

  • j -> 整数

得到:

coilArrayGeneration_Test :: (Floating a, Integral b) => a -> b -> a -> a -> b -> b -> a -> a -> [(a,a,a,b,b)]
coilArrayGeneration_Test r nE width depth n m mu gam
      = [ (x',y',z',i,j)
        | theta <- [0,2*pi/(nE-1)..2*pi]
        , i <- [1..n]
        , j <- [1..m]
        , let a = width/2
        , let b = depth/2
        , let x = r * cos(a*cos(theta)/r)
        , let y = r * sin(a*cos(theta)/r)
        , let z = b * sin(theta)
        , let phi = (2*i-1-n)((a-mu)/r)
        , let zo = (2*j-1-m)(b-gam)
        , let x' = x * cos(phi) - y * sin(phi)
        , let y' = x * sin(phi) + y * cos(phi)
        , let z' = z + zo
        ]

但这引发了一系列错误:

类型声明后的错误

显然,这意味着我不知道在做什么,并且某种程度上弄乱了类型声明。

有谁能指点我正确的方向吗?


当学习Haskell时,我建议以下步骤:1)在编写(函数)定义时,始终从编写其预期类型开始;2)在您的第一批练习中,不要使用比实际需要更通用的类型。即使可以将f :: Int -> Int泛化为f :: Num a => a -> a,最初使用f :: Int -> Int 可能也是可以接受的。 - chi
1个回答

4
当你看到一个编译器错误,涉及到类似Num(c -> c)的东西时,它与-XFlexibleContexts或推断错误的类型无关。这仅仅意味着你试图将某物当作函数使用,但它并不是一个函数。
“被当作函数使用”仅仅意味着你有一些形如f x的表达式,其中fx可以是任意子表达式。这特别包括像(1+2)(3+4)这样的表达式,它相当于:
     let f = 1 + 2
         x = 3 + 4
     in f x

你可能想要用并置表示乘法。那就使用乘法运算符!即(1+2)*(3+4)

你的代码还有另一个问题:你试图在实数表达式中使用索引变量。与缺少乘法运算符不同,这是相当合理的,但Haskell也不允许这样做。你需要显式地将积分包装在fromIntegral中。

coilArrayGeneration_Test r nE width depth n m μ γ
      = [ (x',y',z',i,j)
        | ϑ <- [0, 2*pi/fromIntegral(nE-1) .. 2*pi]
        , i <- [1..n]
        , j <- [1..m]
        , let a = width/2
              b = depth/2
              x = r * cos(a*cos ϑ/r)
              y = r * sin(a*cos ϑ/r)
              z = b * sin ϑ
              φ = fromIntegral(2*i-1-n) * ((a-μ)/r)
              z₀ = fromIntegral(2*j-1-m) * (b-γ)
              x' = x * cos φ - y * sin φ
              y' = x * sin φ + y * cos φ
              z' = z + z₀
        ]

我强烈建议你对代码和类型进行一些重构。5元组非常模糊,你应该至少在适当的向量类型中包装x、y、z


现在我看到了,这就有意义了。所以使用fromX函数将类型转换正确是我之前遗漏的重要部分。非常感谢您的帮助! - ztth222
@ztth222 许多编程语言会自动转换数字类型,例如将整数和双精度浮点数相加会导致整数被提升为双精度浮点数。Haskell则从不这样做,必须在代码中显式使用fromIntegral等函数。总的来说,我认为Haskell对于数值代码来说可能不太方便,但通常更安全:在C中,臭名昭著的例子1/2*x等同于零,但在Haskell中等同于0.5*x - chi
@chi 说实话,这个例子主要展示了C语言的不安全性,而且很奇怪的是,许多编程语言在这方面都效仿了它。Python 3没有这个问题:它的“/”运算符总是像Haskell一样分数形式,但仍然允许整数参数。这在实践中完美地满足了需求。在Haskell中让人讨厌的是,“fromInteger”这个名称太长了。"from"是一个替代方案,我很希望它能被广泛采用。 - leftaroundabout
不过,看起来它有意只将 Int32 转换为 Double,对于 Int 则需要使用 tryFrom。这也是有道理的,但同时也带来了一些麻烦。 - leftaroundabout
在我看来,从 IntDouble 的转换应该是直接可行的,不需要更多的麻烦。我同意 fromIntegral 太长了,但说实话我很少使用它(也许是因为我很少处理浮点数据)。from 可能是一个更好的选择。无论如何,在需要频繁转换的模块中,我们总是可以有一个非公开的 frI = fromIntegral 快捷定义。 - chi
2
@leftaroundabout 我刚意识到,fromtryFrom可能只是用于执行精确、无损转换。如果这是目标,那么很有道理,但我宁愿使用不同的类来转换为浮点数。禁止像tryFloat一样将小数字(如16777216)转换为浮点数,因为它“太大”,而浮点类型支持高达2^128(虽然以有损方式),对于许多应用程序来说过于激进。 - chi

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