Хаскел срещу Перл 6: Първи впечатления

Досега използвах Rakudo Perl 6, за да демонстрирам прости криптографски схеми. Поддържа кодът кратък и сладък и пълен с емоджи. Тъй като Ouroboros е написан на Haskell и изследва алгоритъма на консенсус е смисълът на тази публикация, реших, че трябва поне да натопя пръстите си в нея.

Пренаписах предишното си обръщане на монети с дискретен логаритъм код в Haskell. Версията Perl 6 е тук, а версията Haskell е тук. Те трябва да са сравними един до друг. Ако сте Haskeller, бих се радвал да проверите кода ми и да приветствам дори и най-изтръпналите нитпици.

Струва си да се отбележи, че първата реализация на Perl 6 е написана в Haskell. Можете да забележите някакво влияние върху функционалното програмиране в Perl 6, ако присвивате достатъчно силно.

Това, което следва, е няколко неща, които ме изненадаха или смутиха, докато се опитвах да преведа между двете. Кодът Perl 6 е маркиран с , а Haskell със символа на полуживот λ⃝.

Типове не съществуват по време на изпълнение

Във версията Perl 6 използвам типове по време на изпълнение. Използвах декларация за подмножество, за да изведа подтипове Int с ограничения по време на изпълнение.

# 
# Диапазоните
константа $ ℤ𝑝 = ^ 𝑝; # Perl 6 за 0 .. (𝑝-1);
константа $ ℤ𝑞 = ^ 𝑞;
# Видовете, получени от диапазоните
подмножество ℤ𝑝 на Int: D, където $ ℤ𝑝; # всеки Int в 0 .. (𝑝-1)
подмножество ℤ𝑞 на Int: D, където $ ℤ𝑞;
# Мултипликативната група от ред 𝑞 (генерирана от 𝑔)
подмножество 𝔾 от ℤ𝑝, където * .expmod (𝑞, 𝑝) == 1

Така ℤ𝑞, ℤ𝑞 и 𝔾 се декларират по време на компилиране, но техните ограничения (декларирани с къде) се проверяват по време на изпълнение.

С Haskell не можете да направите същото. Типовете са конструкция само за компилиране; не можете да прикачите ограничения по време на изпълнение към тях. Като се има предвид, че Haskell е функционален, има смисъл да се напишат функции, определящи ограниченията:

- λ⃝
група данни = ℤ𝑝 | ℤ𝑞 | 𝔾 производно (Покажи, Eq)
upperBound :: Group -> Integer
горна граница ℤ𝑝 = 𝑝
горна граница ℤ𝑞 = 𝑞
член :: Group -> Integer -> Bool
член 𝔾 x = (член ℤ𝑝 x) && (expmod x 𝑞 𝑝) == 1
член g x = x> = 0 && x <(горен обвързан g)

Връзката между двата подхода е ясна, като се гледат определенията of един до друг:

# 
подмножество 𝔾 от ℤ𝑝, където * .expmod (𝑞, 𝑝) == 1
- λ⃝
член 𝔾 x = (член ℤ𝑝 x) && (expmod x 𝑞 𝑝) == 1

Perl 6 е по-чист, но версията на Haskell беше добре съставена без специални ключови думи.

Използвам ги за определяне на основната функция на ангажиране, базирана на дискретен логаритъм, така:

# 
под COMMIT (ℤ𝑞 \ 𝑥 -> 𝔾) {expmod (𝑔, 𝑥, 𝑝)}
- λ⃝
ангажиране :: Integer -> Integer
ангажирам 𝑥 | член ℤ𝑞 𝑥 = expmod 𝑔 𝑥 𝑝
         | в противен случай = грешка (покажете 𝑥 ++ „не е член на ℤ𝑞“)

Основното предимство на подмножеството Perl 6 е, че не е нужно да пишете свои собствени съобщения за грешки и подписът изглежда като математическото определение на дискретна функция за ангажиране на логаритъм.

Рекурсия Вместо бримки

Функцията за тайно подканяване приема функцията за анализ на аргумент и връща вход от потребителя, след като бъде успешно обработена от parparse. Версията Perl 6 използва цикъл до, докато версията Haskell използва рекурсия.

# 
подсекретно подсказване (и разбор) {
    до дефинирането ми $ valid = parse (четене на ред) {
        say ("Невалидна стойност. Опитайте отново.")
    }
    върнете $ валиден;
}
- λ⃝
secretPrompt :: (String -> Може би a) -> IO a
secretPrompt разбор = направи
  ред <- readLine
  случай (разбор на линия) на
       Нищо -> putStrLn "Невалидна стойност. Опитайте отново." >>
                  secretPrompt разбор
       Просто x -> връщане x

Императивната версия все още се чувства по-естествена за мен. Предполагам, че мисленето по отношение на функции и рекурсия вместо цикли и променливи просто отнема известно свикване. Не съм сигурен дали версията на Haskell е кратка, колкото би могла да бъде.

Хубавото на Haskell secretPrompt е колко ясно и кратко посочвате аргумента за анализ. В Perl 6 всичко, което мога да кажа, трябва да бъде извиквано с & sigil. В Haskell дефинирам подписа (String -> Може би a) -> IO a, което се превежда на „Аргументната функция за всички обаждания към secretPrompt трябва да вземе String и да върне един от всякакъв тип a. След това secreptPrompt връща IO от типа a ”. Разбира се, без тази функция не бихте могли да получите това за компилиране в Haskell, но все още мисля, че е доста хубаво.

Редактиране: Всъщност има начин да се направи проверка на подписа на повикващия се в Perl 6 по време на изпълнение. Например можете да направите:

подсекретно подсказване (& разбор: (Str -> Any)) {
    до дефинирането ми $ valid = parse (четене на ред) {
        say ("Невалидна стойност. Опитайте отново.")
    }
    върнете $ валиден;
}

Не е толкова мощна, колкото версията на Haskell, която ви позволява да дефинирате типа връщане на функцията по време на компилиране въз основа на това какъв е типът връщане на аргументната функция. Благодаря на Wenzel Peppmeyer, че ми посочи тази функция в коментарите.

Когато Хаскел става досаден

Удовлетворяването на компилатора на Haskell (иначе известно като писане на изрично правилен код) може да бъде наистина сложно. Открих, че го храня с много повтарящи се декларации, за да го поддържам щастлив. Изглежда понякога това е толкова досадно, че разработчиците на Haskell добавят езикови разширения към компилатора, за да облекчат натоварването. Ето как едно наречено „екзистенциално количествено определяне“ се оказа доста удобно.

За всяка стъпка от играта за обръщане на монети Алиса или Роб изпраща на другия съобщение от ключове и стойности като:

# 
 ⟹ (ангажимент => 𝑐, ход => 𝑚);

Това да се работи в Хаскел беше наистина трудно, тъй като because е цяло число и and е монета. С моята обектно ориентирана шапка реших, че всичко, което трябва да направя, е да направя клас като MessageValue, който да определи как трябва да се разпечатва всеки тип неща:

- λ⃝
клас MessageValue a къде
  gist :: a -> String
например MessageValue String където
  gist = id
например MessageValue Integer където
  gist x = "0x" ++ showHex x ""
например MessageValue Coin където
  gist = покажи

И след това направете подписа за ⟹ вземете списък на (String, MessageValue) двойки така:

- λ⃝
(⟹) :: (MessageValue a) => Играч -> [(String, a)] -> IO ()

И го наречете така:

- λ⃝
() ⟹ [(„ход“, 𝑚), („ангажимент“, 𝑐)]

Тъй като типовете 𝑚 и are са и два случая наMessageValue, това трябва да е наред нали? Грешка! Няма значение, че и двете са екземпляри от класа MessageValue, че не са един и същ екземпляр от него. Arrrrg.

И така, как да ги направя един и същ тип? Направих нов тип, който обгръща както Integer, така и Coin така:

- λ⃝
данни MessageWrap = MWS String | MWI Integer | MWC монета
например Message MessageWrap където
gist (MWS x) = gist x
gist (MWI x) = gist x
gist (MWC x) = gist x

И след това го използвайте като:

- λ⃝
() ⟹ [("ход", MWC 𝑚), ("ангажимент", MWI 𝑐)]

Сега имаме списък на (String, MessageValue) двойки и Haskell отново е щастлив и GHC ще състави кода ми. Но това не е идеално, защото всеки път, когато искам да отпечатвам нов тип неща, трябва да създам нова стойност на MW независимо от това и след това да добавя друг повтарящ се съчинител (MWwwhat) кандидат.

За щастие има разширение за компилатор, наречено ExistentialQuantification, което въвежда специален оператор forall, който може да каже на компилатора какво имам предвид в моите по-малко изявления. Използвах го така:

{- # ЕЗИК Екзистенциално количествено определяне # -}
данни MessageWrap = forall a. MessageValue a => MW a
- Определете същност, която разгръща типа
например MessageValue MessageWrap където
  gist (MW m) = gist m

И след това да изпратя съобщението, което просто правя:

() ⟹ [("ход", MW 𝑚), ("ангажимент", MW 𝑐)]

И всичко работи! Страхотен.

... освен когато се прегледах, установих, че според някой, който знае за какво говорят, това е анти-модел . @jonathangfischoff смята, че трябва просто да направя:

() ⟹ [("ход", същност 𝑚), ("ангажимент", същност 𝑐)]

Не съм сигурен съгласен. Това означава, че ако някога искам да променя как работи вътрешно (като например да изпращам комуникациите по мрежата), ще трябва да променя и обаждащия се, което не звучи добре. Бих искал да знам дали това наистина е анти-модел в този случай, защото ми се струва доста добре.

За сравнение, моята версия Perl 6 току-що разширих вградената функция на същността с два кандидата за многократно изпращане.

# 
мулти gist (Int: D $ _) {'0x' ~ .base (16) .lc}
multi gist (изброяване: D $ _) е по подразбиране {.gist}

Изглежда, че цената, която Haskell плаща за наличието на елегантни решения на трудни проблеми, е, че някои прости проблеми като отпечатване на някои неща се превръщат в много по-труден проблем.

Когато се компилира е правилно

Това е, което най-много ме порази в Хаскел. След като преминете през страданието, за да го накарате да компилира, работи! Нямаше нито един път, когато се компилираше и да се проваляше по време на изпълнение. Никога досега не съм имал това преживяване, дори и с други статично типизирани езици. Чистото функционално + статично писане + мощен тип извод изглежда е локален оптимум на езиковия дизайн.

Разбира се, че са необходими много повече усилия, за да се събере всъщност. Получаването на бърза обратна връзка чрез изход и грешки прави динамичните езици по-лесни за изучаване според мен.

Досега попаднах на пристъпи и начала на забавление и безсилие с Haskell. На други езици, които съм научил, обикновено се чувствам доста уверен след седмица да се забърквам с него. Но с Haskell усещам, че едва сега започвам. Мисля, че следващия път трябва да се опитам да го използвам за прилагане на някои проблеми с по-сложни алгоритми и по-малко фини IO.

Отказ от отговорност: Целият код е само за демонстративни цели. Моля, всъщност не използвайте математически символи и емоджи unicode в реален код. Случайните числа не се произвеждат по сигурен начин. Не приемайте статии в интернет като тази като препоръка за прилагане. Не приемайте дори, че е правилно! Както винаги, никога не хвърляйте собствената си криптовалута.