Зависимые типы в haskell: почему это будущее разработки программного обеспечения

Выравнивание кода

Мы встретились с элементами языка, предполагающими группировку определений. В Haskell для этого используется выравнивание: определения из одной группы должны располагаться одно под другим, а сама группа выделяется отступом.

Группировка определений требуется после ключевых слов . ( в действии мы увидим позже; остальные конструкции нам уже знакомы.) Мы уже применяли правильное выравнивание – посмотрите приведенные выше определения с case-of, where и let.

Длинные выражения можно переносить; главное чтобы перенесенная часть начиналась после той колонки, в которой начато определение:

where
  foo = a + b + c + d
          + e + f + g + h
              + i + j + k + l

При необходимости блоки можно выделить явным образом. Группа заключается в фигурные скобки, а определения разделяются точкой с запятой:

s x = case x of {1 -> 4; 2 -> 3; 3 -> 2; 4 -> 1; _ -> 0}

roots a b c = (x1, x2) where {d = b^2-4*a*c; sd = sqrt d; x1 
		= (-b+sd)/(2*a); x2 = (-b-sd)/(2*a)}

Однако на практике достаточно использовать выравнивание, и код с правильным выравниванием легче читать.

Если что-то не так, то ghci выдаст сообщение: «Parse error (possibly incorrect indentation)».

Краткий взгляд на Haskell с зависимыми типами

С повышением функций на уровень типов и квантором , мы сможем переписать следующим образом:

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

Нам все еще приходится переопределять как , чтобы возвращать индуктивно определенный вместо . Пожалуй, эту проблему лучше не рассматривать в рамках добавления зависимых типов в Haskell, ведь Agda тоже использует индуктивно определенный в функции .

В некоторых аспектах Haskell с зависимыми типами даже проще, чем стандартный Haskell. Все-таки в нем термы, типы и виды объединены в общий однородный язык. Я легко могу представить себе написание кода в таком стиле в коммерческом проекте, чтобы формально доказать правильность ключевых компонентов приложений. Многие библиотеки на Haskell смогут предоставить более безопасные интерфейсы без сложности, сопряженной с применением .

Добиться этого будет не просто. Перед нами много инженерных проблем, затрагивающих все компоненты GHC: синтаксический анализатор, разрешение имен, проверку типов, и даже язык Core. Все понадобится модифицировать, или даже полностью перепроектировать.

Тезаурус

Термин Перевод Пояснение
correct by construction Код, корректность которого гарантирована способом его составления Методология разработки, согласно которой корректность кода гарантируется способом его составления (например, применением системы типов), а не тестированием.
memory unsafe С небезопасным доступом к памяти Возможность в языке программирования вручную выделять и освобождать память, выполнять арифметику с указателями.
unityped Монотипизированный Термин, который ввел Bob Harper для более точного описания языков, традиционно называемых динамически типизированными. В таких языках проверки меток типов отложены до времени выполнения программы.
boilerplate Избыточный код Однотипный код с похожими или повторяющимися элементами, который невозможно обобщить из-за недостаточной выразительности языка.
generics Обобщенные типы Возможность системы типов обобщить типы введением параметра. Например, вместо введения конкретных типов «СписокЧисел» и «СписокСтрок», можно ввести обобщенный тип Список, применяя его как Список<Число> и Список<Строка>.
runtime cast Приведение типов времени выполнения Преобразование значения одного типа в значение другого типа с проверкой во время выполнения программы.
effectful computation Вычисление с побочными эффектами Вычисление, выполнение которого приводит к наблюдаемым эффектам помимо возврата вычисленного значения.
composable Композируемый Характеристика кода, отмечающая простоту его применения в другом коде посредством операций композиции.
control structures Конструкции, задающие поток управления Конструкции языка, применение которых влияет на порядок вычисления подвыражений.
proof assistant Средство доказательства теорем Программное обеспечение для формального доказательства математических гипотез.
strict (eager) evaluation Строгие (энергичные) вычисления Стратегия вычисления выражений, согласно которой значения аргументов функции вычисляются до вызова функции.
backend Бэкенд Компонент компилятора, транслирующий внутреннее представление программы в код на целевом языке.
singleton type Единичный тип Тип, населенный одним значением, определяемым инстанцированием соответствующего параметра на уровне типов.
promoted definitions Повышенные определения Определения закрытых семейств типов, которые соответствуют тем или иным функциям на уровне термов.

Generic constructor for records

I have a large number of record types like this, of different length:

data PGD = PGD {
    pgdXUnitBase           :: !Word8,
    pgdYUnitBase           :: !Word8,
    pgdXLUnitsperUnitBase  :: !Word16
}

Currently I use GHC’s Binary module to read them from files; it can handle
types like (Word8, (Word8, Word16)), but there was no easy way to generate
the correct amount of «uncurry» calls for automatically grabbing each element.

With Template Haskell, the instance declarations are now written as such:

instance Binary PGD where
    get bh = do a <- get bh ; return $ $(constrRecord PGD) a

Here the trick lies in constrRecord, which is defined as:

constrRecord x = reify exp where
    reify   = \(Just r) -> appE r $ conE $ last args
    exp     = foldl (dot) uncur $ replicate terms uncur
    terms   = ((length args) `div` 2) - 2
    dot x y = (Just $ infixE x (varE ".") y)
    uncur   = (Just |uncurry|])
    args    = words . show $ typeOf x

— AutrijusTang

Библиотеки

Разбор командной строки, запрос переменных среды и работа с процессами

см. функции getArgs, getEnv, getEnvironment, модули System.Console.GetOpt, System.Exit, System.Cmd, System.Process

Многопоточное программирование

  • см. модули Control.Concurrent.*

Пример:

main = do com  <- replicateM 4 newMVar  -- создаём 4 мьютекса для синхронизации работы с 4 компортами
          files <- mapM ((`fileOpen` WriteMode).show) ..9 -- создаём файлы с именами "0".."9" для записи данных
          mapM_ (forkIO . service com files)    -- запустим отдельный поток для обслуживания каждого прибора
               (, 0x48, 100, )   -- список (номер ком-порта, адрес Modbus, частота сканирования, номер файла),
              , (, 0x88, ,   1)   --   описывающий откуда и с какой частотой читать данные и куда их записывать
              ...
          forever (threadDelay$ 10^6)   -- бесконечный пустой цикл, что позволяет крутиться в фоне потокам обслуживания приборов

-- |Процедура потока, обслуживающего один прибор с заданными параметрами
service com files (port, addr, delay, filenum) = do
    x <- withMVar (com!port) $ \_ -> do             -- блокируем доступ других потоков к этому ком-порту
        ...                                         -- читаем данные из ком-порта
    hPutStrLn (files!filenum) x                     -- записываем данные в файл
    threadDelay delay                               -- ожидаем заданное число микросекунд
    service com files (port, addr, delay, filenum)  -- хвостовая рекурсия используется для организации бесконечного цикла

Comparisons to other languages

Articles contrasting feature of Haskell with other languages.

Mark C. Chu-Carroll, Haskell and Scheme: Which One and Why?
A short overview of similarities and differences between Haskell and Python.
Syntax extension for monads in OCaml
Short intro for perlers
A short blog entry documenting performance results with ruby on rails and Haskell with fastcgi
Paul Hudak and Mark P. Jones, 16 pages.
Ronald Garcia, Jaakko Jrvi, Andrew Lumsdaine, Jeremy G. Siek, and Jeremiah Willcock. In Proceedings of the 2003 ACM SIGPLAN conference on Object-oriented programming, systems, languages, and applications (OOPSLA’03), October 2003.
A list of functional programs applied to real-world tasks. The main criterion for being real-world is that the program was written primarily to perform some task, not primarily to experiment with functional programming. Functional is used in the broad sense that includes both `pure’ programs (no side effects) and `impure’ (some use of side effects). Languages covered include CAML, Clean, Erlang, Haskell, Miranda, Scheme, SML, and others.
Writing A Lisp Interpreter In Haskell, a tutorial
A quick comparison between ruby’s and haskell’s BDD.

Немного про Haskell и как его готовить

Первое, с чем сталкивается каждый разработчик при изучении нового языка — выбор и настройка среды разработки. Конечно, писать можно и в блокноте, но если вы имеете хотя бы незначительный опыт разработки продакшен проектов, такой способ вызовет у вас боль. К стастью, Haskell достаточно старый и распространенный язык и имеет поддержку для большинства известных редакторов и ide. Мой знакомый хаскелист использует emacs. Я привык к нормальным IDE, поэтому поставил плагин для IntelliJ.

Также, для разработки понадобится stack, который сейчас является стандартом и объединяет в себе компилятор, систему управления пакетами, систему сборки и тестирования.

Все выглядит довольно дружелюбно, с установкой проблем не возникло. Для разработки я использую Mac OS, на других системах не проверял, но подозреваю, что под Linux тоже все заведется без проблем.

‘generic’ zipWith

A generalization of zipWith to almost any data. Demonstrates the ability to do dynamic binding with TH splices (note ‘dyn’).

zipCons :: Name -> Int -> String -> ExpQ
zipCons tyName ways functions = do
    let countFields :: Con -> (Name,Int)
        countFields x = case x of
            NormalC n (length -> fields) -> (n, fields)
            RecC n (length -> fields) -> (n,fields)
            InfixC _ n _ -> (n,2)
            ForallC _ _ ct -> countFields ct

    TyConI (DataD _ _ _ countFields -> (c,n)] _) <- reify tyName
    when (n /= length functions) $ fail "wrong number of functions named"
    vs <- replicateM ways $ replicateM n $ newName "x"
    lamE (map (conP c . map varP) vs) $
        foldl (\con (vs,f) ->
                  con `appE`
                    foldl appE
                        (dyn f)
                        (map varE vs))
              (conE c)
              (transpose vs `zip` functions)

This example uses whichever ‘+’ is in scope when the expression is spliced:

type $(zipCons ''(,,,) 2 (replicate 4 "+"))

  $(zipCons ''(,,,) 2 (replicate 4 "+"))
    :: (Num t, Num t1, Num t2, Num t3) =>
        (t, t1, t2, t3) -> (t, t1, t2, t3) -> (t, t1, t2, t3)

Функции и процедуры

В императивных языках вроде C++ нет разделения функций на чистые и имеющие побочные эффекты, любая функция рассматривается как потенциально «грязная». С одной стороны, это облегчает модификацию программы (к любой чисто вычислительной функции могут быть добавлены побочные эффекты), с другой стороны — усложняет понимание программы, её отладку и модификацию. Какая-нибудь скромная функция sin может иметь совершенно нескромные побочные эффекты, например стереть системные файлы.

В отличие от них, в Haskell все функции чётко поделены на два класса. Для удобства дальнейшего изложения давайте условимся называть чистые функции просто функциями, а нечистые — процедурами. Итак, функция — это просто однозначный способ вычисления выходного значения по входным, а процедура выполняет некоторое действие (хотя может иметь и выходное значение).

Вычисления внутри функций производятся по мере необходимости и в том порядке, в каком в них возникает необходимость. В отличие от этого процедура описывает последовательность операций, которые выполняются обязательно и обязательно в указанном порядке. Поэтому способ записи, применяемый для определения функций, не годится для процедур, и для них используется специальная do-нотация, сходная с императивными языками (как С++ или Python).

Функции не могут вызывать процедуры, и это означает, что Haskell гарантирует отсутствие побочных эффектов в чистых вычислениях. По своему опыту могу сказать, что в первое время программировать с этим ограничением было неудобно, но потом привыкаешь и начинаешь просто думать по-другому, автоматически разделяя в уме алгоритмы чистых вычислений и императивную логику программы с тем, чтобы записать их отдельно друг от друга.

Нюанс std::min({…})

Деталь номер один, которую заметил сам автор и множество людей в комментах, это использование std::min.

Смотрим на хаскель версию:

Как ни странно, тут как раз и есть два раза вызов функции от двух аргументов и в том же порядке! (надеюсь, на таком уровне я понимаю хаскель правильно). Таким образом, автор, после исправления C++ версии, сам получает, что один llvm равен второму llvm. Собственно, это я ожидал с самого начала. К сожалению, предположение, что «Наskell может давать больше хинтов компилятору» не подтвердилось. Но судьба сложилась так, что изначально автор статьи «Быстрее чем С++; медленне, чем PHP» проверил эту замену только на гцц, а этому компилятору от этого ни холодно, ни жарко. Как видно в бенчмарке ниже:

Компилятор Время оригнала Время исправленного (1)
haskell/llvm 910ms
gcc 9.2 1211ms 1211ms
clang 9 1915ms 852ms

Примечание: = conditional move (условие: — greater, — less equal, и т.д.).

Но что интересно, в случае, когда вовлечены указатели, это не так, и clang, в отличии от gcc, зачем-то кладёт данные на стек (туда указывает rsp регистр):

Что косвенно объясняет, почему clang более чувствителен к этому изменению в исходном коде. Также в случае без компилятор генерирует оптимальный asm уже при , а с ним нужно вовлечь больше оптимизаций (), чтобы добиться того же asm выхлопа. Таким образом, не совсем зеро-кост, тут создателям стд-либы, возможно, стоит подумать над перегрузками для некоторых эвристик.

Условия эксперимента

Итак, сначала обсудим экспериментальную установку.

Данные

Кстати, размещается на -разделе, и оперативной памяти более чем достаточно, так что здесь нет никакого дискового IO.

Железо и софт

Всё это счастье запускается под Gentoo Linux на Core i7 4770 с 32 гигабайтами оперативной памяти.

Для хаскель-кода используется ghc 8.8.2.

взят из coreutils 8.31, собранных gcc 9.2 с :

Кстати, этот вот — лёгкая поблажка в пользу C, так как хаскель-код собирается без каких-либо аналогичных опций, что имеет свои преимущества: полученный бинарник сможет работать на любой x86_64-машине, тогда как из coreutils может работать только на моём процессоре и более новых (если компилятор решил воспользоваться соответствующими SIMD-инструкциями, конечно же).

Кроме того, я пробовал собирать руками с вместо — результаты различаются несущественно, так что оставим дефолтовый .

Измерения

Измерения производятся просто, используя . показывает время и в юзерспейсе, и в ядре, но мы будем сравнивать только первое, так как

  1. время в ядре с хорошей точностью и стабильностью равно примерно 0.3 секунды,
  2. я не бенчмаркаю ядро, и меня интересует лишь то, сколько времени выполняется мой код.

Как и с любым другим полностью детерминированным бенчмарком, каждый исполняемый файл запускается несколько (в данном случае пять) раз, и записывается минимальное время из всех запусков.

Bird Style

In Bird-style you have to leave a blank before the code.

> fact :: Integer -> Integer
> fact  = 1
> fact n = n * fact (n-1)

And you have to leave a blank line after the code as well.

The idea behind this restriction is capturing the mistake of not inserting the mark at the beginning of the line. In general this is not only good practice, but also a formatting that makes the code more readable.

However, there are cases in which you might like to get around this restriction. Perhaps you’re writing Haskell code within a markup language that’s not Latex, and you may have to surround your code with something equivalent to and . In this case, GHC provides a flag that can be used to lift the blank lines requirement:

$ ghc -optL -q

Handling Options with Templates

A common idiom for treating a set of options, e.g. from GetOpt, is to define a datatype with all the flags and using a list over this datatype:

data Options = B1 | B2 | V Integer

options = B1, V 3

While it’s simple testing if a Boolean flag is set (simply use «elem»), it’s harder to check if an option with an argument is set. It’s even more tedious writing helper-functions to obtain the value from such an option since you have to explicitely «un-V» each. Here, Template Haskell can be (ab)used to reduce this a bit. The following example provides the module «OptionsTH» which can be reused regardless of the constructors in «Options». Let’s start with showing how we’d like to be able to program. Notice that the resulting lists need some more treatment e.g. through «foldl».

Options.hs:

module Main where

import OptionsTH
import Language.Haskell.TH.Syntax

data Options = B1 | B2 | V Int | S String deriving (Eq, Read, Show)

options = B1, V 3

main = do
  print foo -- test if B1 set:               
  print bar -- test if V present, w/o value: 
  print baz -- get value of V if available:  

foo :: Bool
-- Query constructor B1 which takes no arguments
foo = map $(getopt (THNoArg (mkArg "B1" ))) options

bar :: Bool
-- V is a unary constructor. Let mkArg generate the required
-- wildcard-pattern "V _".
bar = map $(getopt (THNoArg (mkArg "V" 1))) options

-- Can't use a wildcard here!
baz :: 
baz = map $(getopt (THArg (conP "V" varP "x"]))) options

OptionsTH.hs

module OptionsTH where

import Language.Haskell.TH.Syntax

-- datatype for querying options:
-- NoArg: Not interested in value (also applies to Boolean flags)
-- Arg:   Grep value of unary(!) constructor
data Args = THNoArg Pat | THArg Pat

getopt :: Args -> ExpQ
getopt (THNoArg pat) = lamE varP "y" (caseE (varE "y") cons0, cons1])
 where
  cons0 = match pat   (normalB | True  |]) []
  cons1 = match wildP (normalB | False |]) []

-- bind "var" for later use!
getopt (THArg pat@(ConP _ VarP var])) = lamE varP "y" (caseE (varE "y") cons0, cons1])
 where
  cons0 = match pat   (normalB (appE |Just| (varE var))) []
  cons1 = match wildP (normalB |Nothing|]) []

mkArg :: String -> Int -> Pat
mkArg k c = conP k (replicate c wildP)

While the example might look contrived for the Boolean options which could have been tested much easier, it shows how both types of arguments can be treated in a similar way.

Limitations

getopt (THArg pat) is only able to treat unary constructors. See the pattern-binding: It matches exactly a single VarP.

Improvements

The following reduces things even a bit more, though I still don’t know if I like it. It only works since c is either 0 or 1.

mkArg k c = conP k (replicate c (varP "x"))

baz = map $(getopt (THArg (mkArg "V" 1)))

— VolkerStolz

Не только из двух

Часто мы хотим выбирать не только из двух возможных вариантов. Вот как это можно сделать:

Уверен, вы уже стираете плевок с экрана. Вложенная конструкция не может понравиться никому, ведь она крайне неудобна в обращении. А уж если бы анализируемых проб золота было штук пять или семь, эта лестница стала бы поистине ужасной. К счастью, в Haskell можно написать по-другому:

Не правда ли, так красивее? Это — множественный . Работает он по схеме:

где — выражения, дающие ложь или истину, а — соответствующие им результирующие выражения. Особая функция соответствует общему случаю, когда ни одно из логических выражений не дало , и в этой ситуации результатом условной конструкции послужит выражение .

Не пренебрегайте ! Если вы его не укажете и при этом примените функцию к значению, отличному от проверяемых:

компиляция завершится успешно, однако в момент запуска программы вас ожидает неприятный сюрприз в виде ошибки:

Проверка получилась неполной, вот и получите ошибку.

Кстати, видите слово в сообщении об ошибке? Вертикальные черты перед логическими выражениями — это и есть охранники (англ. guard), неусыпно охраняющие наши условия. Потешное название выбрали. Чтобы читать их было легче, воспринимайте их как аналог слова «ИЛИ».

А сейчас стоп

Вы ведь попробовали скомпилировать этот код, не так ли? А почему вы не ругаетесь? Ведь такой код не скомпилируется, так как не хватает одной маленькой, но важной детали. Вот как должен выглядеть модуль :

Вот теперь всё в порядке. Но что это за странный комментарий в первой строке модуля? Вроде бы оформлен как многострочный комментарий, но выглядит необычно. Перед нами — указание расширения языка Haskell.

Стандарт Haskell 2010 — это официальный стержень языка. Однако компилятор GHC, давно уж ставший компилятором по умолчанию при разработке на Haskell, обладает рядом особых возможностей. По умолчанию многие из этих возможностей выключены, а прагма как раз для того и предназначена, чтобы их включать/активизировать. В данном случае мы включили расширение . Именно это расширение позволяет нам использовать множественный . Такого рода расширений существует очень много, и мы будем часто их использовать.

Помните: расширение, включённое с помощью прагмы , действует лишь в рамках текущего модуля. И если я прописал его только в модуле , то на модуль механизм не распространяется.

lhs2TeX

Highly recommended is , courtesy of Andres Löh. It is designed for typesetting papers about Haskell, but lhs2TeX is easily configured and can make for a powerful preprocessor and documentation generator.

Input to lhs2TeX is a slightly modified file. One would typically use the standard latex recommendations above, using a and pair to demarcate code. Additionally, lhs2TeX provides specialized macros to control the preprocessing.

Note that lhs2TeX and in-line commenting do not seem to mix well.

Since it can typeset Haskell formulas in mathematical notation
with LaTeX’s math mode, you can also use it to create testable
papers. That is, readers can play with the formulas presented in the
paper if they obtain the literate Haskell source code for the paper.

Applications Written in Haskell

An implementation of Perl6.
The DoCon computer algebra system implements a good-sized piece of commutative algebra.
One of the project aims is to test the efficiency of a real-world CA
system based on pure functionality and «lazy» computation.
This is a study in combining Computer algebra, Term rewriting and Automatic proofs. The system is presented as a library of Haskell functions.
A mail transport agent (MTA) written in Haskell. The primary focus of the project is to provide an MTA that’s a lot more flexible than the current implementations, in particular when it comes to implementing junk mail countermeasures.
AMuZed is a graphical editing tool for the creation of mu-charts or microcharts. mu-charts consist of the ‘well-defined’ part of the Statecharts found in UML — in that they have a semantics and, via the Z translation, a logic too. ZooM is a tool which takes the .muz file output of a created chart from AMuZed and generates a series of .tex files which contain the Z specification for that chart.
A tool that processes first-order logic problems and tries to find finite-domain models for them.
A tool for downloading RDF site summaries and showing news in a text file and in an HTML file.
David’s Advanced Revision Control System is yet another replacement for CVS. It is written in Haskell, and has been tested on Linux and MacOS X. darcs includes a cgi script, which can be used to view the contents of your repository.
Currently David offers a LaTeX preprocessor.
An application for construction and running of finite state transducers.
An asteroids-like game.
A project that uses Haskell to build interpreters from monadic semantic descriptions.
Knit is a component definition and linking language for systems programming based on the Unit component programming model. Knit lets you turn ordinary C code (e.g., bits of the Linux kernel) into components and link them together to build new programs. Since the freedom to do new things brings with it the freedom to make new errors, Knit provides a simple constraint system to catch component configuration errors. Knit also provides a cross-component inliner and schedules initialization and finalization of components.
Knit is released under a BSD-style license, is written in Haskell (and a little C) and includes a C parser and pretty-printer. A useful little utility included in the distribution is a tool for renaming symbols in ELF-format object files.
The current Knit release acts post-compilation: we compile C code as normal and then rename symbols in object files before linking. We are rewriting Knit to act pre-compilation: manipulating the source code before compilation. This will bring the much needed ability to import and export types from modules.
A heart-beat monitor for local network links for Linux.
We have used this attribute grammars sytem in the construction of our haskell compilers. It has proven to be an indispendable tool in keeping the various aspects apart.
Truth is a platform for the verification of distributed systems.
TSL is a language for describing hierarchies of schedulers and reasoning about what kinds of locks can legally be used at each level and what race conditions those locks can prevent.
A large IRC bot, dynamically extensible via plugins.
Frag
A first person 3D game written in Haskell.
An extensible text editor, reminiscent of vim.
A curses-based mp3 player.
Hoogle
A Haskell API search engine
A commercial Android game using SDL2 multimedia written in the FRP flavour Yampa.
A breakout game with Kinect and Wiimote support, SDL multimedia, written in the FRP DSL Yampa.
A Mac application that allows the user to define classes and enums in an editor and provides the ability to generate Objective-C source code for them, that contain functions for serializing/deserializing to/from JSON.

Без Если

Множественный весьма удобен, но есть способ более красивый. Взгляните:

Ключевое слово исчезло. Схема здесь такая:

Устройство почти такое же, но, помимо исчезновения ключевого слова , мы теперь используем знаки равенства вместо стрелок. Именно поэтому исчез знакомый нам знак равенства после имени аргумента . В действительности он, конечно, никуда не исчез, он лишь перешёл в выражения. А чтобы это легче прочесть, напишем выражения в строчку:

То есть перед нами уже не одно определение функции, а цепочка определений, потому нам и не нужно ключевое слово . Но и эту цепочку определений можно упростить.

Для любопытных

До появления основным способом установки Haskell была так называемая Haskell Platform. Однако именно , несмотря на свою молодость (вышел в свет летом 2015 года), является предпочтительным путём в мир Haskell, особенно для новичков. Его настолько полюбили, что последние версии Haskell Platform включают в себя по умолчанию!

Как вы заметили, имена файлов с исходным кодом начинаются с большой буквы: и . Строго говоря, это необязательно, можно и с маленькой буквы, однако для гармонии с именем модуля лучше придерживаться общепринятой практики и называть файл модуля по имени самого модуля:

И ещё. При создании проекта мы могли бы использовать схему вместо предлагаемой по умолчанию. Для этого проект нужно было создать командой:

где — имя схемы проекта. Дело в том, что команда может создавать заготовки проектов для разных нужд. Простейшая из заготовок называется . В этом случае в проекте отсутствует модуль , а есть лишь :

Да, мы могли бы воспользоваться данной схемой, однако в этом случае мы не увидели бы механизма импорта одного модуля в другой. Я рад, что вы познакомились с импортом уже сейчас, ведь в последующих главах мы будем постоянно использовать различные модули из многих библиотек.

Заключение

Мы рассмотрели важные элементы Haskell, которые используются при определении функций, а также специальные выражения для построения списков.

Приведенные примеры функционального кода позволяют проследить использование рекурсии, функций высокого порядка и бесконечных структур данных.

В общем случае, при функциональном подходе:

  • Сложное вычисление должно выражаться через композицию простых функций.
  • Каждая функция отвечает за свой небольшой элемент вычислений.
  • Параметры вычислений выносятся в аргументы функций.
  • В соответствии с частичным применением, сначала должны следовать более общие параметры.
  • Функции должны быть как можно более абстрактными и общими, пригодными для повторного использования. Часто используемые шаблоны можно оформлять в виде функций высокого порядка.

В качестве упражнения по определению функций и применению рекурсии вы можете записать свои аналоги для (их описание содержится в предыдущей статье).

По аналогии с описанной в тексте попробуйте определить , которая будет вычислять начальный список элементов, удовлетворяющих данному предикату.

Запишите другие полезные функции для работы со списками:

iterate f x =   (бесконечный список)
repeat x =   (бесконечный список)
cycle  =   (n >= 1)
replicate n x =   (n элементов)

Вы можете свериться с прилагаемым к статье файлом.

В следующей статье мы рассмотрим работу с алгебраическими типами данных.

Прилагаемые файлы:fph03.lhs, fph03ex.lhs

Похожие темы

  • Haskell 98 Language and Libraries. The Revised Report. 2002. (EN)
  • P. Hudak, J. Peterson, J. Fasel. A Gentle Introduction to Haskell 98. 2000. (EN)
  • G. Hutton. Programming in Haskell. Cambridge University Press, 2007. (EN)
  • R. Plasmeijer, M. van Eekelen. Functional Programming and Parallel Graph Rewriting. Addison-Wesley. 1993. (EN)
  • • B. O’Sullivan, D. Stewart, J. Goerzen. Real World Haskell. (EN)
Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Adblock
detector