Сопоставление языков программирования Часть 7. Haskell

Comments

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, Вася

Ресурсы для скачивания


Похожие темы

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Open source
ArticleID=969251
ArticleTitle=Сопоставление языков программирования Часть 7. Haskell
publish-date=04222014