Сопоставление языков программирования Часть 7. Haskell
Haskell — стандартизованный чистый функциональный язык программирования общего назначения. Другие обсуждаемые функциональные языки Ocaml или Scala — считаются смешанными языками, так как помимо функционального поддерживают и императивный стиль вычислений. Haskell не допускает императивного программирования. Является одним из самых распространённых языков программирования с поддержкой отложенных вычислений. Типизация языка Haskell строгая, статическая, с автоматическим выводом типов. Строгое отношение к типизации (что не характерно, вообще говоря, для функциональных языков) — ещё одна отличительная черта Haskell. Поскольку язык функциональный, то основная управляющая структура — это функция.
В 1990 г. была предложена первая версия языка, Haskell 1.0. Непосредственно на него оказал очень сильное влияние язык Miranda, разработанный в 1985 г. Дэвидом Тёрнером (Миранда была первым чистым функциональным языком). Но выход Haskell в «широкий свет» начался только в 2003 г. — таким образом, в течение 13 лет этот язык был уделом лабораторий, главным образом математически ориентированных.
Порог вхождения в программирование на Haskell высок. Во-первых, из-за его происхождения из кругов абстрактных математиков и из-за формулирования его понятий в терминах понятий из абстрактной математики (теории категорий). Другая причина, связанная с предыдущей, из-за которой и сложилось устойчивое ложное представление о колоссальной сложности языка Haskell, — это отсутствие внятных описаний и руководств. А официальная документация Haskell выкладывается также в виде строгих формальных определений, на изучение которых могут уйти месяцы. Например, одно из важных понятий и терминов в языке — «монада» (от греческого μονάς, «единица»), пришедшие в Haskell именно из теории категорий. Вслушаемся, как звучит его определение в официальной документации (даже в переводе на русский язык):
Монада может быть определена через общее понятие моноида в моноидальной категории. Монада над категорией K — это моноид в моноидальной категории эндофункторов End(K).
С таким же успехом это определение можно было бы перевести на китайский! Тем не менее, описать монады «на пальцах» можно достаточно просто, а пользоваться ими может любой средний практик, слегка освоившись с их применением. На сегодня есть уже некоторое количество руководств, относительно пригодных для начального освоения языка (см. указатель ресурсов в конце текста).
С другой стороны, Haskell является языком, строго организующим мышление программиста. В ряде ведущих университетов мира именно Haskell выбран как первый язык обучения студентов 1-го курса «искусству программирования» (определение Д.Кнутта).
Существует несколько реализаций Haskell доступных в Linux, но компилятор GHC стал фактическим стандартом в отношении новых возможностей языка:
$ yum list ghc Доступные пакеты ghc.i686 7.6.3-18.3.fc20 updates $ sudo yum install ghc ... Установить 1 пакет (+47 зависимых) Объем загрузки: 82 M Объем изменений: 610 M ... Установлено: ghc.i686 0:7.6.3-18.3.fc20 $ ghc --version The Glorious Glasgow Haskell Compilation System, version 7.6.3
Легко видеть, что объём изменений эта инсталляция потянет значительный. В пакете будет установлен одновременно диалоговый интерпретатор Haskell, полезный для отработки конструкций языка, он же может выполнять отдельные приложения:
# ghci GHCi, version 7.6.3: http://www.haskell.org/ghc/ :? for help Loading package ghc-prim ... linking ... done. Loading package integer-gmp ... linking ... done. Loading package base ... linking ... done. Prelude> 2+2 4 Prelude> ^D Leaving GHCi.
Но Haskell — это целая своеобразная технология, и в этой технологии есть ещё такая штука как cabal (Common Architecture for Building Applications and Libraries). Говорят, что это нечто типа make в мире C/C++ (я бы сравнил скорее с Cmake) — построитель проектов Haskell. Но cabal придётся устанавливать отдельно:
$ yum list cabal* Загружены модули: langpacks, refresh-packagekit Доступные пакеты cabal-dev.i686 0.9.2-2.fc20 fedora cabal-install.i686 1.16.0.2-27.fc20 updates cabal-rpm.i686 0.8.7-1.fc20 updates $ sudo yum install cabal* ... Установить 3 пакета (+13 зависимых) Объем загрузки: 1.7 M Объем изменений: 9.1 M ... Выполнено!
Кроме того, что это инструмент построения собственных модульных проектов, это мощнейшее средство управления модулями (библиотеками) Haskell:
$ cabal --help ... Commands: install Installs a list of packages. update Updates list of known packages list List packages matching a search string. info Display detailed information about a particular package. fetch Downloads packages for later installation. unpack Unpacks packages for user inspection. check Check the package for common mistakes sdist Generate a source distribution file (.tar.gz). upload Uploads source packages to Hackage report Upload build reports to a remote server. init Interactively create a .cabal file. configure Prepare to build the package. build Make this package ready for installation. copy Copy the files into the install locations. haddock Generate Haddock HTML documentation. clean Clean up after a build. hscolour Generate HsColour colourised code, in HTML format. register Register this package with the compiler. test Run the test suite, if any (configure with UserHooks). bench Run the benchmark, if any (configure with UserHooks). upgrade (command disabled, use install instead) help Help about commands ... $ cabal update Downloading the latest package list from hackage.haskell.org Note: there is a new version of cabal-install available. To upgrade, run: cabal install cabal-install
С помощью cabal вы можете найти, выбрать и установить любой модуль (библиотеку) из главного мирового репозитория Haskell (его называют Hackage):
$ cabal list complex * complex-generic Synopsis: complex numbers with non-mandatory RealFloat Default available version: 0.1.1 Installed versions: [ Not installed ] Homepage: https://gitorious.org/complex-generic License: BSD3 * complex-integrate Synopsis: A simple integration function to integrate a complex-valued complex functions Default available version: 1.0.0 Installed versions: [ Not installed ] Homepage: https://github.com/hijarian/complex-integrate License: PublicDomain * complexity Synopsis: Empirical algorithmic complexity Default available version: 0.1.3 Installed versions: [ Not installed ] License: BSD3 * storable-complex Synopsis: Storable instance for Complex Default available version: 0.2.1 Installed versions: [ Not installed ] License: BSD3
Библиотека Haskell очень обширна (1-я команда выведет полный листинг библиотеки):
$ cabal list >> cabal.lst $ ls -l cabal.lst -rw-rw-r--. 1 Olej Olej 1273606 мар 9 11:40 cabal.lst $ wc -l cabal.lst 41520 cabal.lst
Учитывая, что информация о каждом модуле выводится в 6 строк (см. выше) — это даёт объём библиотеки в 6920 единиц компиляции.
Файлы исходного кода Haskell имеют расширения .hs или .lhs. О дним из фундаментальных свойств языка Haskell, которым программисты пугают друг друга, является полное отсутствие в нём оператора присваивания. В написании реализации нашей тестовой задачи, в отношении Haskell мы пойдём другим путём, отличающимся от того, как это делалось в других языках: мы не станем вручную компилировать из командной строки файл кода triangle.hs, а создадим проект, пользуясь возможностями cabal по созданию и управлению проектами.
Примечание: Конечно, вы можете откомпилировать полученный ниже файл кода задачи и вручную, предварительно переименовав его в triangle.hs .
Создадим для начала вручную файловую инфраструктуру проекта, например так:
$ mkdir triangle $ cd triangle $ mkdir src $ cd src $ touch Main.hs $ cd .. $ tree . └── src └── Main.hs
Теперь, находясь в корне дерева проекта (каталог triangle), выполним построение проекта командой, которая проведёт диалог настройки проекта, вопросы которого достаточно понятны:
$ cabal init Package name? [default: triangle] Package version? [default: 0.1.0.0] Please choose a license: * 1) (none) 2) GPL-2 3) GPL-3 4) LGPL-2.1 5) LGPL-3 6) BSD3 7) MIT 8) Apache-2.0 9) PublicDomain 10) AllRightsReserved 11) Other (specify) Your choice? [default: (none)] 7 Author name? Maintainer email? Project homepage URL? Project synopsis? Project category: * 1) (none) 2) Codec 3) Concurrency 4) Control 5) Data 6) Database 7) Development 8) Distribution 9) Game 10) Graphics 11) Language 12) Math 13) Network 14) Sound 15) System 15) System 16) Testing 17) Text 18) Web 19) Other (specify) Your choice? [default: (none)] What does the package build: 1) Library 2) Executable Your choice? 2 Include documentation on what each field means (y/n)? [default: n] Guessing dependencies... Generating LICENSE... Warning: unknown license type, you must put a copy in LICENSE yourself. Generating Setup.hs... Generating triangle.cabal... Warning: no synopsis given. You should edit the .cabal file and add one. You may want to edit the .cabal file and add a Description field. $ tree . ├── Setup.hs ├── src │ └── Main.hs └── triangle.cabal
В каталоге src могут быть созданы дополнительные файлы кода к проекту (.hs) или каталоги (например Utils), содержащие такие файлы. Дополнительные файлы кода будут содержать код модулей, которые компонуются в проект. Имена файлов и каталогов в src лучше именовать с заглавной буквы — это связано с именованием и импортом модулей в Haskell.
После генерации в файловой иерархии появилось 2 файла: Setup.hs, который нас не интересует, и файл конфигурации проекта triangle.cabal, в котором мы будем неоднократно редактировать строки по ходу развития проекта (сами параметры строк уже записаны в файл в виде комментариев, нам предстоит раскомментировать их и вписать им значения). Прежде всего, нужно (обязательно) определить файл кода с которого стартует приложение (Main.hs, он может иметь произвольное имя):
... executable triangle ghc-options: -W main-is: Main.hs build-depends: haskell98 >=2.0.0.2 , exceptions ...
Здесь показаны только строки, подвергшиеся изменению (в порядке, в котором они указаны) в том исходном файле triangle.cabal, который был создан при построении проекта:
- определение включить вывод предупреждений компиляции, не только ошибок;
- определить файл Main.hs как стартовый (на самом деле имена файлов кода могут быть произвольными);
- описать импорт дополнительных стандартных пакетов (библиотек): в данном случае пакет haskell98 содержит модуль Complex для работы с комплексными числами, а пакет exceptions — обработку исключений;
Теперь нам осталось сконфигурировать проект под наши правки (конфигурацию лучше делать каждый раз после редактирования triangle.cabal):
$ cabal configure Resolving dependencies... Configuring triangle-0.1.0.0... Warning: The 'license-file' field refers to the file 'LICENSE' which does not exist.
Всё! Проект готов. Дальше нам предстоит наполнять смыслом файлы исходного кода (Main.hs) и компилировать проект. Вот как может выглядеть код сравниваемой задачи в упрощённой реализации на Haskell (упрощение касается только отсутствия обработки ошибок ввода пользователя — чтобы не перегружать код):
Листинг 6. Реализация задачи на языке Haskell (файл Main.hs каталог triangle):
module Main where import Complex import Numeric import IO {- код проверен для версии: $ ghc --version The Glorious Glasgow Haskell Compilation System, version 7.6.3 -} type Point = Complex Double -- синоним координатной точки get_coord :: String -> Point -- декодирование строки ввода в координаты x % y get_coord str = f( words str ) where f :: [String] -> Point f lst = ( ( read( lst !! 0 ) :: Double ) :+ ( read( lst !! 1 ) :: Double ) ) try_to_input :: IO String -- ввод строки с ожиданием ^D try_to_input = do line <- hGetLine stdin `catch` (\e -> if IO.isEOFError e then return [] else ioError e) return line get_shape :: [Point] -> IO [Point] get_shape shape = do -- рекурсивный ввод списка вершин let pos = length( shape ) + 1 putStr( "вершина № " ++ show( pos ) ++ " : " ) hFlush stdout line <- try_to_input if length( line ) == 0 then return shape else get_shape( get_coord( line ) : shape ) showP = \p -> "[" ++ ( showFFloat ( Just 2 ) ( realPart( p ) ) "" ) ++ "," ++ ( showFFloat ( Just 2 ) ( imagPart( p ) ) "" ) ++ "] " show_shape :: [Point] -> String show_shape (x:xs) = if length xs == 0 then showP x else ( showP x ) ++ show_shape( xs ) perimeter :: [Point] -> Double perimeter shape = summa shape 0.0 where distance = \ p1 p2 -> magnitude( p1 - p2 ) summa :: [Point] -> Double -> Double summa (y:ys) perim -- локальная функция накопления длин сторон | length( ys ) == 0 = ( distance y $ head shape ) + perim | otherwise = ( distance y $ head ys ) + summa ys perim square :: [Point] -> Double square (y:ys) = summa y ys 0.0 where summa :: Point -> [Point] -> Double -> Double summa top shape squa -- локальная функция накопления площади | length( tail shape ) == 0 = squa | otherwise = ( triang top shape ) + summa top ( tail shape ) squa triang :: Point -> [Point] -> Double triang top (z:zs) = -- локальная функция площадь треугольника ( magnitude side1 ) * ( magnitude side2 ) * ( abs $ sin( phase side1 - phase side2 ) ) * 0.5 where side1 = z - top side2 = z - head zs next_shape :: IO () -- цикл рассчёта next_shape = do putStrLn( "координаты вершин в формате: X Y" ) shape <- get_shape [] putStrLn $ "\rвершин " ++ show( length shape ) ++ " : " ++ show_shape shape putStrLn( "периметр = " ++ showFFloat ( Just 2 ) ( perimeter shape ) "" ) putStrLn( "площадь = " ++ showFFloat ( Just 2 ) ( square shape ) "" ) putStrLn $ "---------------------------------" next_shape main :: IO () main = do next_shape -- запуск цикла программы
Исходный код на Haskell не является форматно независимым: его смысл зависит от отступов новых строк, переносов строк и других вещей, связанных с размещением кода. Это достаточно редкий случай для языков программирования, и здесь (только в написании) Haskell близок с Python.
В показанном фрагменте кода есть достаточно много: и лямбда-определения функций, и сопоставления с образцом, и обработка исключений (в определении конца ввода, ситуации EOF), и использование рекурсии.
Теперь мы готовы компилировать полученный проект:
$ cabal build Building triangle-0.1.0.0... Preprocessing executable 'triangle' for triangle-0.1.0.0... [1 of 1] Compiling Main ( src/Main.hs, dist/build/triangle/triangle-tmp/Main.o ) src/Main.hs:3:1: Warning: The import of `Numeric' is redundant except perhaps to import instances from `Numeric' To import instances alone, use: import Numeric() src/Main.hs:42:1: Warning: Pattern match(es) are non-exhaustive In an equation for `show_shape': Patterns not matched: [] src/Main.hs:50:10: Warning: Pattern match(es) are non-exhaustive In an equation for `summa': Patterns not matched: [] _ src/Main.hs:55:1: Warning: Pattern match(es) are non-exhaustive In an equation for `square': Patterns not matched: [] src/Main.hs:62:10: Warning: Pattern match(es) are non-exhaustive In an equation for `triang': Patterns not matched: _ [] Linking dist/build/triangle/triangle ...
Почему так много предупреждений? Не знаю... Могу предположить по смыслу, что все они (связанные только с сопоставлением с образцом) используют синтаксические конструкции, заимствованные из описаний прежних версий (Haskell 98). А очень свежий компилятор (Haskell 2010) хотел бы более современных определений образцов. Предоставим читателям возможность и право самостоятельно улучшить код в этом направлении.
После правок, конфигураций и компиляций дерево проекта имеет структуру:
$ tree . ├── dist │ ├── build │ │ ├── autogen │ │ │ ├── cabal_macros.h │ │ │ └── Paths_triangle.hs │ │ └── triangle │ │ ├── triangle │ │ └── triangle-tmp │ │ ├── Main.hi │ │ └── Main.o │ ├── package.conf.inplace │ └── setup-config ├── Setup.hs ├── src │ └── Main.hs └── triangle.cabal
Здесь поддерево dist и есть, собственно, каталогом сборки. Запускать откомпилированное приложение на тестирование мы можем прямо из каталога проекта:
$ ./dist/build/triangle/triangle координаты вершин в формате: X Y вершина № 1 : 1.00001 1.00003 вершина № 2 : 1.00003 2.0003 вершина № 3 : 2.0004 1.00005 вершин 3 : [2.00,1.00] [1.00,2.00] [1.00,1.00] периметр = 3.42 площадь = 0.50 --------------------------------- координаты вершин в формате: X Y вершина № 1 : ^C
Как видим, оно не сильно отличается от того, что мы видели в реализациях на других языках программирования.
После построения проекта, симметрично, очищаем следы его создания:
$ cabal clean cleaning...
Для того, чтобы не быть голословным относительно ручной компиляции отдельных файлов Haskell кода, о которой упоминалось выше, продемонстрируем его на простейшем приложении, которое заодно проверит, как реализация языка ведёт себя с Unicode, кодировкой UTF-8 и русским текстом (это всегда нужно делать для начала). Само «приложение»:
Листинг 7. Простейшее приложение на языке Haskell:
module Main where import System.Environment main :: IO () main = do -- сама программа args <- getArgs {- вложенный комментарий -} putStrLn( "Привет от Haskell, " ++ args !! 0 )
Его ручная компиляция
$ ghc -o hello_hs hello_hs.hs [1 of 1] Compiling Main ( hello_hs.hs, hello_hs.o ) Linking hello_hs ...
Ручное выполнение:
$ ./hello_hs Вася Привет от Haskell, Вася
Ресурсы для скачивания
- этот контент в PDF
- Архив программных кодов (ex7.tgz | 264KB)
Похожие темы
- Haskell
- О Haskell по-человечески, март 2014
- Язык Haskell: О пользе и вреде лени
- Язык и библиотеки Haskell 98
- Haskell 2010. Language Report
- Пол Хьюдак, Джон Петерсон, Джозеф Фасел: Мягкое введение в Haskell, пер. Денис Москвин Часть 1
- Пол Хьюдак, Джон Петерсон, Джозеф Фасел: Мягкое введение в Haskell, пер. Денис Москвин Часть 2
- Основы функционального программирования / Основы языка Haskell