Хаскелл розкладає просту "середню" функцію


76

Я бавлюся з початківцем Хаскеллом, і я хотів написати середню функцію. Це здавалося найпростішим у світі, чи не так?

Неправильно.

Здається, що система типу Хаскелла забороняє в середньому працювати над загальним числовим типом - я можу змусити його працювати у списку інтегралів або списку фракціонів, але не в обох.

Я хочу:

average :: (Num a, Fractional b) => [a] -> b
average xs = ...

Але я можу отримати лише:

averageInt :: (Integral a, Fractional b) => [a] -> b
averageInt xs = fromIntegral (sum xs) / fromIntegral (length xs)

або

averageFrac :: (Fractional a) => [a] -> a
averageFrac xs = sum xs / fromIntegral (length xs)

а другий, здається, працює. Поки я не спробую передати змінну.

*Main> averageFrac [1,2,3]
2.0
*Main> let x = [1,2,3]
*Main> :t x
x :: [Integer]
*Main> averageFrac x

<interactive>:1:0:
    No instance for (Fractional Integer)
      arising from a use of `averageFrac ' at <interactive>:1:0-8
    Possible fix: add an instance declaration for (Fractional Integer)
    In the expression: average x
    In the definition of `it': it = averageFrac x

Очевидно, Хаскелл справді вибагливий до своїх типів. Що має сенс. Але не тоді, коли вони обидва могли бути [Num]

Чи мені не вистачає очевидного застосування RealFrac?

Чи є спосіб примусити інтеграли до дробових частин, які не задихаються, коли він отримує дробове введення?

Чи є спосіб використовувати Eitherі eitherстворити якусь середню поліморфну ​​функцію, яка працювала б на будь-якому числовому масиві?

Чи система типу Хаскелла прямо забороняє цю функцію коли-небудь існувати?

Навчання Haskell - це все одно, що вивчити Калькуляцію. Це дійсно складно і базується на теоретичних горах, а іноді проблема настільки складна, що я навіть не знаю, щоб правильно сформулювати питання, тому будь-яке розуміння буде тепло прийняте.

(Крім того, виноска: це базується на проблемі домашнього завдання. Усі згодні з тим, що середній Frac, наведений вище, отримує повні бали, але у мене є підступна підозра, що є спосіб зробити так, щоб він працював як на інтегральних, так і на дробових масивах)


Відповіді:


106

Отже, принципово, ви обмежені типом (/):

(/) :: (Fractional a) => a -> a -> a

До речі, ви також хочете Data.List.genericLength

genericLength :: (Num i) => [b] -> i

То як щодо видалення fromIntegral для чогось більш загального:

import Data.List

average xs = realToFrac (sum xs) / genericLength xs

який має лише реальне обмеження (Int, Integer, Float, Double) ...

average :: (Real a, Fractional b) => [a] -> b

Отже, це призведе будь-який Реал до будь-якого дробу.

І зверніть увагу на всі плакати, які потрапляють у поліморфні числові літерали в Хаскелі. 1 не є цілим числом, це будь-яке число.

Клас Real забезпечує лише один метод: можливість перетворити значення в класі Num на раціональне. Що саме те, що нам тут потрібно.

І, таким чином,

Prelude> average ([1 .. 10] :: [Double])
5.5
Prelude> average ([1 .. 10] :: [Int])
5.5
Prelude> average ([1 .. 10] :: [Float])
5.5
Prelude> average ([1 .. 10] :: [Data.Word.Word8])
5.5

але що, якщо я хочу назвати середнім у списку, скажімо, парних? Чи існує така функція, як numToFrac, яка приймає або Real, або Fractional, і повертає Fractional? Чи могли б ми написати одну?
jakebman

5
Ви можете подати його до списку парних, оскільки Double є в Real. "середнє ([1 .. 10] :: [Подвійне])". Клас Real додає саме можливість побудувати раціональне значення з речей у Num. Це саме те, що вам потрібно.
Дон Стюарт,

Ти маєш рацію! Дякуємо, що це пояснили! Чи є в Num такі типи, на яких realToFrac не працює? Я не можу зрозуміти, чому це не numToFrac.
jakebman 04.03.10

4
Неможливо написати numToFrac, оскільки Num не надає жодних функцій перетворення. Реальне - це найближче, що ми маємо (типи Num, які можна перетворити на Rational), або Integral (типи Num, які можна перетворити на необмежені цілі числа).
Дон Стюарт

Дякую! Я ввів в оману діаграму на останній сторінці cs.ut.ee/~varmo/MFP2004/PreludeTour.pdf, яка показує, що Floating НЕ успадковує властивості від Real, і тоді я припустив, що вони не мають спільних типів. Принаймні я можу вчитися на своїх помилках.
jakebman

25

Донс дуже добре відповів на це запитання, я подумав, що можу щось додати.

При розрахунку середнього значення таким чином:

average xs = realToFrac (sum xs) / genericLength xs

Що зробить ваш код, це двічі перейти список, один раз обчислити суму його елементів і один раз отримати його довжину. Наскільки мені відомо, GHC ще не може оптимізувати це і обчислити як суму, так і довжину за один прохід.

Навіть новачкові не заважає думати про це та про можливі рішення, наприклад, середня функція може бути записана за допомогою складки, яка обчислює як суму, так і довжину; на ghci:

:set -XBangPatterns

import Data.List

let avg l=let (t,n) = foldl' (\(!b,!c) a -> (a+b,c+1)) (0,0) l in realToFrac(t)/realToFrac(n)

avg ([1,2,3,4]::[Int])
2.5
avg ([1,2,3,4]::[Double])
2.5

Функція виглядає не такою елегантною, але продуктивність краща.

Більше інформації в блозі Dons:

http://donsbot.wordpress.com/2008/06/04/haskell-as-fast-as-c-working-at-a-high-altitude-for-low-level-performance/


4
+1 за пропозицію складання, щоб отримати кращий приріст продуктивності, але я б рекомендував спробувати виконати лише після того, як ви повністю зрозумієте Haskell і поліморфізм цілих чисел для вас - дитяча гра.
Роберт Массайолі,

11
+1 за коментар Роберта Массайолі, оскільки це середнє значення насправді дуже погане ... foldl 'суворий в акумуляторі, що для Хаскелла означає "слабку голову в нормальній формі", сувору до першого конструктора даних. Так, наприклад, тут foldl 'гарантуватиме, що накопичувач буде оцінений достатньо, щоб визначити, що це пара (перший конструктор даних), але вміст не оцінюватиметься, і тому накопичує хитрості. Використання foldl' (\(!b,!c) a -> ...або строгий тип пари data P a = P !a !aє ключовим фактором для досягнення хорошої продуктивності в цьому випадку.
Джедай

+1 за коментар Джедая, я відповідно відредагував допис.
Девід В.

9

Оскільки Донс зробив настільки гарну роботу, відповідаючи на ваше запитання, я попрацюю над допитом вашого питання ....

Наприклад, у вашому запитанні, де ви вперше запустили середнє значення в даному списку, отримавши хорошу відповідь. Потім ви берете те, що схоже на той самий список , присвоюєте його змінній, а потім використовуєте функцію змінної ..., яка потім підриває.

Що ви зіткнулися тут, підлаштовано в компіляторі називається DMR: D прочитаного M onomorphic R estriction. Коли ви передали список безпосередньо у функцію, компілятор не припустив, якого типу є числа, він просто зробив висновок, які типи він може базуватися на використанні, а потім вибрав один, коли він більше не міг звузити поле. Це щось на зразок прямої протилежності набору качок.

У будь-якому випадку, коли ви присвоїли список змінній, DMR почав діяти. Оскільки ви помістили список у змінну, але не дали натяків на те, як ви хочете її використовувати, DMR змусив компілятор вибрати тип, у цьому випадок, він вибрав один , який відповідав формі і відчував себе: Integer. Оскільки ваша функція не може використовувати ціле число у своїй /роботі (їй потрібен тип уFractional класі), вона подає цю саму скаргу: Integerу Fractionalкласі немає жодного екземпляра . Є варіанти, які ви можете встановити в GHC, щоб він не примушував ваші значення в єдину форму ("мономорфне", зрозуміло?), Доки це не потрібно, але будь-які повідомлення про помилки дещо жорсткіші для з'ясування.

Тепер, в іншій записці, у вас була відповідь на відповідь Донса, яка потрапила мені в очі:

Я ввів в оману діаграму на останній сторінці cs.ut.ee/~varmo/MFP2004/PreludeTour.pdf, яка показує, що Floating НЕ успадковує властивості від Real, і тоді я припустив, що вони не мають спільних типів.

Haskell робить типи не так, як ти звик. Realі Floatingє класами типів, які працюють більше як інтерфейси, ніж класи об’єктів. Вони говорять вам, що ви можете робити з типом, що входить до цього класу, але це не означає, що деякий тип не може робити інші речі, так само, як наявність одного інтерфейсу означає, що клас (у стилі OO) не може є будь-які інші.

Навчання Haskell - це все одно, що вивчити Калькуляцію

Я б сказав, що вивчення Haskell - це все одно, що вивчати шведську - є багато маленьких простих речей (букв, цифр), які виглядають і працюють однаково, але є також слова, схожі на те, що вони повинні означати одне, коли вони насправді щось означають ще. Але як тільки ви вільно ним володієте, ваші постійні друзі будуть вражені тим, як ви можете виливати цю дивну штуку, яка змушує чудових красунь робити дивовижні трюки. Цікаво, що в Хаскелі є багато людей, які з самого початку беруть участь, які також знають шведську. Можливо, ця метафора - це більше, ніж просто метафора ...



-6

Так, система типу Хаскелла дуже вибаглива. Проблема тут полягає у типі fromIntegral:

Prelude> :t fromIntegral
fromIntegral :: (Integral a, Num b) => a -> b

fromIntegral буде лише приймати інтеграл як, а не якийсь - або інший вид Num. (/), з іншого боку, приймає лише дробові. Як ви хочете змусити двох працювати разом?

Ну, функція суми - це хороший початок:

Prelude> :t sum
sum :: (Num a) => [a] -> a

Сума бере список будь-яких чисел і повертає число.

Ваша наступна проблема - довжина списку. Довжина - це Int:

Prelude> :t length
length :: [a] -> Int

Вам також потрібно перетворити цей Int на число. Ось що робить відIntegral.

Отже, у вас є функція, яка повертає Num, і ще одна функція, яка повертає Num. Існує кілька правил для просування типів номерів, які ви можете переглянути , але в основному на цьому етапі все готово:

Prelude> let average xs = (sum xs) / (fromIntegral (length xs))
Prelude> :t average
average :: (Fractional a) => [a] -> a

Давайте пробний запуск:

Prelude> average [1,2,3,4,5]
3.0
Prelude> average [1.2,3.4,5.6,7.8,9.0]
5.4
Prelude> average [1.2,3,4.5,6,7.8,9]
5.25

11
Ви також потрапили в ту ж пастку, що і Майкл. Числове перевантаження! 5 не є інтегральним значенням. Це будь-яке число. Тут за замовчуванням використовується дробове значення - ви не можете передати Int або Integer. - як ви не отримаєте жодного примірника для (Fractional Int)
Дон Стюарт

Так, мені там погано. Я не приділяв достатньо пильної уваги. Я думаю, саме в цьому причина, чому мені ніколи не вдалося зробити набагато більше, ніж програмування іграшок в Haskell. Навіть як шанувальник мов неволі та дисципліни, я вважаю Хаскелла трохи жорстоким майстром.
ТІЛЬКИ МОЙ правильний ДУМКА

2
Хаскелл не є B&D. Підходити до нього як до B&D буде боляче. Якщо ви хочете вивчити Haskell, вам потрібно буде освоїти систему шрифтів. Це все. Ваша плутанина зникне до того моменту, коли ви зрозумієте, як використовувати типи для вас .
номен
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.