Пытаясь окунуться в функциональное программирование, я пытаюсь освоить Haskell и столкнулся с некоторыми психологическими проблемами с системой типов.
Выполнение следующего кода дает правильный вывод (например, генерирует координаты для окружности, обернутой вокруг цилиндра радиуса 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
Однако, пытаясь обобщить это в функцию, которая генерирует произвольный массив NxM кругов с различными перекрытиями в полярных и z-направлениях, запустив:
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],....
После некоторого поиска в Google мне показалось, что моя функция имеет неправильный тип, подразумеваемый компилятором, но я, к сожалению, недостаточно хорошо понимаю идею определения типа Haskell, чтобы исправить ее. Я попытался определить типы так, как я их вижу, а именно:
г -> Двойной
nE -> Целое
ширина -> двойной
глубина -> двойной
п -> Целое
м -> Целое
мю -> Двойной
игра -> Двойной
х ' -> Двойной
у' -> Двойной
г' -> Двойной
Я -> Целое
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
]
Но это вызвало целый ряд ошибок:
Что явно означает, что я не знаю, что делаю, и как-то испортил объявления типов.
Может ли кто-нибудь направить меня на правильный путь?
Когда вы видите ошибку компилятора, связанную с чем-то вроде Num (c -> c)
, она никогда не имеет ничего общего с -XFlexibleContexts
или с неправильными выводами типов. Это просто означает, что вы пытались использовать что-то в качестве функции, которая не является функцией.
«Использовать как функцию» означает просто, что у вас есть некоторое выражение в форме f x
, где f
и x
могут быть произвольными подвыражениями. Это включает, в частности, такие выражения, как (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 Многие языки автоматически преобразовывают числовые типы, например суммирование целого числа и двойного приводит к тому, что целое число повышается до двойного. Вместо этого Haskell никогда этого не делает, а fromIntegral
и тому подобное должны явно использоваться в коде. В целом, я нахожу Haskell немного менее удобным для числового кода, но часто гораздо более безопасным: печально известный пример 1/2*x
эквивалентен нулю в C, но вместо этого эквивалентен 0.5*x
в Haskell.
@chi, если честно, этот пример в основном показывает, насколько C небезопасен, и немного странно, что в этом отношении ему последовало так много языков. В Python 3 этой проблемы нет: его оператор /
всегда дробный, как в Haskell, но он по-прежнему допускает целые аргументы. На практике это работает вполне удовлетворительно. Что раздражает в Haskell, так это то, что fromInteger
такая громоздкая штука. from — это альтернатива, которую я бы очень хотел, чтобы она стала широко адаптированной.
... хотя похоже, что он намеренно преобразует только Int32
в Double
, для Int
требуется tryFrom
. Что на самом деле тоже имеет смысл, но также влечет за собой собственные неудобства.
@leftaroundabout ИМО, от Int
до Double
должно просто работать без необходимости прыгать дальше. Я согласен с тем, что fromIntegral
слишком длинный, но, честно говоря, мне никогда не приходилось использовать его слишком часто (возможно, потому, что я редко имею дело с данными с плавающей запятой). from
может быть лучшим вариантом. В любом случае, всегда можно иметь неэкспортируемое определение ярлыка frI = fromIntegral
в модулях, которые требуют частых преобразований.
@leftaroundabout Я только что понял, что, вероятно, from
и tryFrom
предназначены только для точных преобразований без потерь. Если это цель, это имеет смысл, но я бы предпочел использовать другой класс для преобразования в числа с плавающей запятой. Запрещение преобразования небольшого числа, такого как 16777216
, в число с плавающей запятой, потому что оно «слишком велико», как это делает tryFloat
, когда тип float поддерживает до 2^128
(хотя и с потерями), является слишком радикальным для многих приложений.
При изучении Haskell я бы рекомендовал следующее: 1) при написании определения (функции) всегда начинайте с написания ее предполагаемого типа, 2) в ваших первых упражнениях не используйте более общие типы, чем вам действительно нужно. Вероятно, сначала можно использовать
f :: Int -> Int
, даже если его можно обобщить доf :: Num a => a -> a
.