Основы работы с Haskell

Хаскель — чисто-функциональный язык общего назначения, соединяющий недавние разработки в области проектирования языков программирования. Хаскель предоставляет функции высшего порядка, нестрогую семантику, статическую полиморфическую типизацию, определённые пользователем алгебраические типы данных, сопоставление с образцом, большие возможности при работе со списками, массивами, целыми числами с произвольной или фиксированной точностью и числами с плавающей точкой. См. Haskell 2010 Language Report.

Код, написанный на Хаскель, можно запустить по-разному:

  1. в режиме интерпретации;
  2. скомпилировав в исполняемый файл;
  3. запуск кода без компиляции;
  4. использование систем сборки.

Существуют различные реализации спецификации языка Хаскель. Рекомендуемой на официальном сайте является платформа GHC (Glasgow Haskell Compiler). Другие реализации, такие как UHC, LHC, а также различные диалекты языка (например Frege, компилируемый в байт код Java) использоваться в курсе не будут.

Режим интерпретации

Режим интерпретации запускает интерпретатор в консоли, в котором выражения вводятся непосредственно в командной строке. Для запуска интерпретатора вызывается команда ghci:

markoutte$ ghci
GHCi, version 8.0.2: http://www.haskell.org/ghc/  :? for help
Prelude> :help

Для выхода из режима интерпретации необходимо выполнить команду :q или нажать Ctrl + D.

Найдём корни квадратного уравнения:

Prelude> :{
Prelude| findRoots a b c = if d < 0 then error "No roots" else (x1, x2)
Prelude|   where
Prelude|     d = b ^ 2 - 4 * a * c
Prelude|     x1 = (-b - sqrt d) / (2 * a)
Prelude|     x2 = (-b + sqrt d) / (2 * a)
Prelude| :}
Prelude> print $ findRoots 1 2 1
(-1.0,-1.0)

Команды :{ и :} позволяют обозначить начало многострочного кода. Полный список команд интерпретатора можно узнать набрав команду :help. При этом Хаскель автоматически выведет тип аргументов заданной функции:

Prelude> :t findRoots
findRoots :: (Ord t, Floating t) => t -> t -> t -> (t, t)
Prelude>

Такую запись следует читать следующим образом: <именование> :: <ограничения> => <аргументы функции и их типы>. Таким образом, применить функцию для целых типов не получится:

Prelude> a = 2 :: Int
Prelude> b = 3 :: Int
Prelude> c = 1 :: Int
Prelude> findRoots a b c

:26:1: error:
    • No instance for (Floating Int) arising from a use of ‘findRoots’
    • In the expression: findRoots a b c
      In an equation for ‘it’: it = findRoots a b c

Компиляция в исполняемый файл

Создадим 2 файла: Main.hs для основной программы и Math.hs для математических функций:

module Main where

import Math

main = print (findRoots 2 4 1)
module Math where

findRoots a b c = if d < 0 then error "No roots" else (x1, x2)
  where
    d = b ^ 2 - 4 * a * c
    x1 = (-b - sqrt d) / (2 * a)
    x2 = (-b + sqrt d) / (2 * a)	

Скомпилируем и запустим код:

markoutte$ ghc Main.hs
[1 of 2] Compiling Math             ( Math.hs, Math.o )
[2 of 2] Compiling Main             ( Main.hs, Main.o )
Linking Main ...
markoutte$ ls -l
total 3784
-rwxr-xr-x  1 markoutte  staff  1901680 19 фев 22:32 Main
-rw-r--r--  1 markoutte  staff      848 19 фев 22:32 Main.hi
-rw-r--r--  1 markoutte  staff       62 19 фев 22:29 Main.hs
-rw-r--r--  1 markoutte  staff     3304 19 фев 22:32 Main.o
-rw-r--r--  1 markoutte  staff     1002 19 фев 22:32 Math.hi
-rw-r--r--  1 markoutte  staff      181 19 фев 22:28 Math.hs
-rw-r--r--  1 markoutte  staff    10128 19 фев 22:32 Math.o
markoutte$ ./Main
(-1.7071067811865475,-0.2928932188134524)

В то время, как .o файлы являются скомпилированными в бинарный формат модулями (объектными модулями), файлы .hi хранят информацию о самом модуле и его зависимостях. Посмотреть, что там хранится можно, использовав команду ghc --show-iface Main.hi. Подробней об этом можно прочитать в статье Using GHC.

Запуск без компиляции

Код на хаскеле можно запустить без компиляции, выполнив следующие команды:

markoutte$ runhaskell Main.hs
(-1.7071067811865475,-0.2928932188134524)
Eltanin:example markoutte$ ls -l
total 16
-rw-r--r--  1 markoutte  staff   62 19 фев 22:29 Main.hs
-rw-r--r--  1 markoutte  staff  181 19 фев 22:28 Math.hs

Использование Cabal Stack

Cabal — это система сборки, поставляемая вместе с платформой Хаскель. На сегодняшний день (февраль 2017) набирает популярность другая система сборки и менеджер зависимостей — Stack. Работа с ним хорошо описана в статье «Прощай, cabal. Здравствуй, stack!» и «Practical haskell - getting started with stack».

Например, чтобы запустить простой вебсервер на Yesod, достаточно выполнить следующие команды:

markoutte$ stack new test-server yesod-minimal
markoutte$ cd test-server/
markoutte$ stack build
markoutte$ stack exec test-server
markoutte$ stack exec test-server
19/Feb/2017:23:18:12 +0300 [Info#yesod-core] Application launched @(yesod-core-1.4.31-DLiX005mSrG8ItA7Sozqvl:Yesod.Core.Dispatch ./Yesod/Core/Dispatch.hs:165:11)
127.0.0.1 - - [19/Feb/2017:23:20:19 +0300] "GET / HTTP/1.1" 200 202 "" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/602.4.8 (KHTML, like Gecko) Version/10.0.3 Safari/602.4.8"
127.0.0.1 - - [19/Feb/2017:23:20:19 +0300] "GET /favicon.ico HTTP/1.1" 404 118 "http://localhost:3000/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/602.4.8 (KHTML, like Gecko) Version/10.0.3 Safari/602.4.8"
test-server: Thread killed by Warp's timeout reaper

После этого доступ к странице можно получить по адресу http://localhost:3000. Редактирование зависимостей от других библиотек происходит в файле .cabal. Подробнее об этом в User guide. Например, чтобы успешно собрать и запустить следующий пример, взятого для разработки пользовательского интерфейса на базе веб технологий:

{-----------------------------------------------------------------------------
    threepenny-gui
    
    Example:
    Small database with CRUD operations and filtering.
    To keep things simple, the list box is rebuild every time
    that the database is updated. This is perfectly fine for rapid prototyping.
    A more sophisticated approach would use incremental updates.
------------------------------------------------------------------------------}
{-# LANGUAGE RecursiveDo #-}

import Prelude hiding (lookup)
import Control.Monad  (void)
import Data.List      (isPrefixOf)
import Data.Maybe
import Data.Monoid
import qualified Data.Map as Map
import qualified Data.Set as Set

import qualified Graphics.UI.Threepenny as UI
import Graphics.UI.Threepenny.Core hiding (delete)

{-----------------------------------------------------------------------------
    Main
------------------------------------------------------------------------------}
main :: IO ()
main = startGUI defaultConfig setup

setup :: Window -> UI ()
setup window = void $ mdo
    return window # set title "CRUD Example (Simple)"

    -- GUI elements
    createBtn   <- UI.button #+ [string "Create"]
    deleteBtn   <- UI.button #+ [string "Delete"]
    listBox     <- UI.listBox  bListBoxItems bSelection bDisplayDataItem
    filterEntry <- UI.entry    bFilterString
    ((firstname, lastname), tDataItem)
                <- dataItem    bSelectionDataItem

    -- GUI layout
    element listBox # set (attr "size") "10" # set style [("width","200px")]
    
    let uiDataItem = grid [[string "First Name:", element firstname]
                          ,[string "Last Name:" , element lastname]]
    let glue = string " "
    getBody window #+ [grid
        [[row [string "Filter prefix:", element filterEntry], glue]
        ,[element listBox, uiDataItem]
        ,[row [element createBtn, element deleteBtn], glue]
        ]]

    -- events and behaviors
    bFilterString <- stepper "" . rumors $ UI.userText filterEntry
    let tFilter = isPrefixOf <$> UI.userText filterEntry
        bFilter = facts  tFilter
        eFilter = rumors tFilter

    let eSelection  = rumors $ UI.userSelection listBox
        eDataItemIn = rumors $ tDataItem
        eCreate     = UI.click createBtn
        eDelete     = UI.click deleteBtn

    -- database
    -- bDatabase :: Behavior (Database DataItem)
    let update' mkey x = flip update x <$> mkey
    bDatabase <- accumB emptydb $ concatenate <$> unions
        [ create ("Emil","Example") <$ eCreate
        , filterJust $ update' <$> bSelection <@> eDataItemIn
        , delete <$> filterJust (bSelection <@ eDelete)
        ]

    -- selection
    -- bSelection :: Behavior (Maybe DatabaseKey)
    bSelection <- stepper Nothing $ head <$> unions
        [ eSelection
        , Nothing <$ eDelete
        , Just . nextKey <$> bDatabase <@ eCreate
        , (\b s p -> b >>= \a -> if p (s a) then Just a else Nothing)
            <$> bSelection <*> bShowDataItem <@> eFilter
        ]
                
    let bLookup :: Behavior (DatabaseKey -> Maybe DataItem)
        bLookup = flip lookup <$> bDatabase
                
        bShowDataItem :: Behavior (DatabaseKey -> String)
        bShowDataItem = (maybe "" showDataItem .) <$> bLookup

        bDisplayDataItem = (UI.string .) <$> bShowDataItem
                
        bListBoxItems :: Behavior [DatabaseKey]
        bListBoxItems = (\p show -> filter (p. show) . keys)
                    <$> bFilter <*> bShowDataItem <*> bDatabase

        bSelectionDataItem :: Behavior (Maybe DataItem)
        bSelectionDataItem = (=<<) <$> bLookup <*> bSelection

    -- automatically enable / disable editing
    let
        bDisplayItem :: Behavior Bool
        bDisplayItem = maybe False (const True) <$> bSelection
    
    element deleteBtn # sink UI.enabled bDisplayItem
    element firstname # sink UI.enabled bDisplayItem
    element lastname  # sink UI.enabled bDisplayItem


{-----------------------------------------------------------------------------
    Database Model
------------------------------------------------------------------------------}
type DatabaseKey = Int
data Database a  = Database { nextKey :: !Int, db :: Map.Map DatabaseKey a }

emptydb = Database 0 Map.empty
keys    = Map.keys . db

create x     (Database newkey db) = Database (newkey+1) $ Map.insert newkey x db
update key x (Database newkey db) = Database newkey     $ Map.insert key    x db
delete key   (Database newkey db) = Database newkey     $ Map.delete key db
lookup key   (Database _      db) = Map.lookup key db

{-----------------------------------------------------------------------------
    Data items that are stored in the data base
------------------------------------------------------------------------------}
type DataItem = (String, String)
showDataItem (firstname, lastname) = lastname ++ ", " ++ firstname

-- | Data item widget, consisting of two text entries
dataItem
    :: Behavior (Maybe DataItem)
    -> UI ((Element, Element), Tidings DataItem)
dataItem bItem = do
    entry1 <- UI.entry $ fst . maybe ("","") id <$> bItem
    entry2 <- UI.entry $ snd . maybe ("","") id <$> bItem
    
    return ( (getElement entry1, getElement entry2)
           , (,) <$> UI.userText entry1 <*> UI.userText entry2
           )

достаточно добавить в файл .cabal следующие зависимости: threepenny-gui >= 0.7.0.1, сontainers >= 0.5.7.1.

Материал дополняется, поэтому любые комментарии и исправления приветствуются.

Сообщите об опечатке, выделив ошибку в тексте и нажав на Ctrl + Enter.

Что ещё почитать?

Learn You a Haskell for Great Good!

Hey yo! This is Learn You a Haskell, the funkiest way to learn Haskell, which is the best functional programming language around. You may have heard of it. This guide is meant for people who have programmed already, but have yet to try functional programming.

http://learnyouahaskell.com

Изучаем Haskell

От издателя Эта книга поможет вам быстро освоить базовые концепции языка программирования Haskell, его библиотеки и компоненты, а также заложит основы функциональной парадигмы программирования, которая становится все более значимой в современном мире разработки ПО. Книга предлагает проектный подход к освоению материала, используя в качестве прототипа проект реализации интернет-магазина. Здесь рассматривается экосистема языка Haskell и его вспомогательных средств, инструменты Cabal для управление проектами, модули HUnit и QuickCheck для тестирования программ, фреймворк Scotty для разработки веб-приложений, Persistent и Esqueleto – для управления базами данных и многие другие компоненты и библиотеки Haskell.

https://www.ozon.ru/context/detail/id/35161141/

What i wish i knew when learning haskell

http://dev.stephendiehl.com/hask

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: