Haskell достаточно сильно отличается от типичных императивных языков программирования: недоступность явного оператора присваивания (роднящая Haskell с такими языками как Erlang, Prolog и Clojure), статическая система типов (отличающая Haskell от вышеупомянутых языков) и ленивые вычисления по-умолчанию (среди языков программирования общего назначения – достаточно уникальное явление) накладывают свою специфику на решение типичных для программирования задач.
Тем не менее, иногда императивное изложение алгоритма является наиболее простым и понятным. В этой главе показаны как идиоматические для Haskell способы выражения императивных алгоритмов, так и способы эмуляции императивных конструкций в тех ситуациях, когда без них трудно обойтись.
В большинстве императивных языков явное ветвление осуществляется одним из двух способов: конструкцией if .. else .. наподобие
# Python
if foo:
command1
command2
else if bar:
command3
command4
else if baz:
command5
else:
command6или же существенно реже встречающейся таблицей переходов:
// C
switch foo {
case 1:
command1
command2
break;
case 3:
command3
command4
break;
default:
command5
break;
}Хотя в Haskell и присутствует выражение if .. then .. else .., аналогичное тернарному оператору .. ? .. : .. языка C, используется оно обычно с той же частотой, что и упомянутый тернарный оператор в C (довольно редко). Чаще можно увидеть case-выражения или же уравнения с несколькими правыми частями. А поскольку тип Bool в Haskell определён как
data Bool = True | Falseто периодически вместо if foo then bar else baz используется
case foo of
True -> bar
False -> bazВ императивных частях кода (внутри do-блоков) можно увидеть комбинаторы when и unless из модуля Control.Monad:
when True bar = bar
when False _ = pure ()
unless = when . notОни же – оператор goto и его более «мягкие» аналоги типа return, break и continue. Haskell не поощряет написание императивного кода. Тем не менее, библиотека mtl предоставляет типы Cont и ContT (о том, как они определены, см. следующую главу), позволяющие эмулировать поведение этих операторов.
Этот раздел, вероятно, следует читать после прочтения нескольких следующих.
Типичный код на императивном языке
k = 2
while k < n:
if n % k == 0: break
k += 1
if k < n: print('Делитель нашёлся!')Код на Haskell с использованием Cont (буквальный перевод):
loop n k break = do
when (n `mod` k == 0) $ break k
loop n (k+1) break
check n = do
k <- flip runCont id $ callCC (loop n 2)
when (k < n) $ putStrLn "Делитель нашёлся!"И, наконец, как надо:
unless (null $ dropWhile ((0 /=) . mod n) [2..n-1]) $
putStrLn "Делитель нашёлся!"Скажем несколько слов о том, что такое runCont/runContT и как их использовать. Комбинатор runCont добавляет к вычислению монадный интерфейс (см. следующую главу) и операцию callCC. Операция callCC передаёт в функцию операцию преждевременного завершения (аналог return в императивных языках). В отличие от императивных языков, операция преждевременного завершения может быть передана ещё какой-нибудь функции.
Комбинатор runContT «надевает» на монадный интерфейс свой монадный интерфейс, отвечающий за изменение ячейки. Действия внутреннего монадного интерфейса можно преобразовывать в действия внешнего комбинатором lift модуля Control.Monad.Trans:
// довольно дурацкий пример на C++
bool hasEven(vector<int> &numbers) {
for (auto n : numbers) {
if (n % 2 == 0) {return true;}
}
return false;
}
int findEven(vector<int> &numbers) {
for (auto n : numbers) {
if (n % 2 == 0) {return n;}
}
return 0;
}
int main() {
int n;
cin >> n;
vector<int> numbers(n);
for (unsigned i = 0; i != n; i++) {
cin >> numbers[i];
}
if (hasEven(numbers)) {
cout << "Первое чётное число: " << findEven(numbers) << endl;
} else {
cout << "Нечётных чисел не найдено" << endl;
}
}-- Почти дословный перевод на Haskell.
-- Единственная разница: вместо массива -- список.
hasEven numbers break = do
for_ numbers $ \n -> when (n `mod` 2 == 0) (break True)
break False
findEven numbers break = do
for_ numbers $ \n -> when (n `mod` 2 == 0) (break n)
break 0
main = do
text <- getContents
let n:rest = read <$> words text :: [Int]
numbers = take n rest
flip runContT pure $ do
found <- callCC (hasEven numbers)
case found of
True -> do
x <- callCC (findEven numbers)
lift $ putStrLn ("Первое чётное число: " ++ x)
False -> lift $ putStrLn "Нечётных чисел не найдено" Они же – циклы. Большая часть циклов относится к одному из двух видов:
Типичный пример первого:
squares_sum = 0
for i in range(n):
squares_sum += n**2Типичный пример второго:
for i,n in enumerate(numbers):
print(i,"->",n)Циклы первого типа в Haskell в большинстве случаев выражаются комбинатором foldl' (из модуля Data.Foldable):
let squaresSum = foldl' (\a x -> a + x^2) 0 [0..n-1]Циклы второго типа в Haskell выражаются комбинаторами traverse_ или for_ (из того же модуля Data.Foldable):
for_ (zip [0..] numbers) $ \(i,n) -> do
putStrLn $ show i ++ " -> " ++ show n
-- Комбинатор traverse_ отличается от for_ только порядком аргументов:
-- traverse_ x y = for_ y xВ императивных языках одна из ключевых возможностей – возможность изменять значения переменных. В функциональных языках изменение значений переменных не поощряется (или вообще невозможно). Вместо этого используются разнообразные ячейки: объекты с операциями их модификации.
Для однопоточного приложения в подавляющем большинстве случаев достаточно комбинаторов runState и runStateT из модуля Control.Monad.State.Strict пакета mtl. Первый из них добавляет к вычислению монадный интерфейс и одну ячейку:
let (_, sum) = flip runState 0 $ do
for_ [1..10] $ \x -> modify (\s -> s+x)Второй «надевает» на монадный интерфейс свой монадный интерфейс, отвечающий за изменение ячейки. Действия внутреннего монадного интерфейса можно преобразовывать в действия внешнего комбинатором lift модуля Control.Monad.Trans:
main = do
text <- getLine
let n = read text :: Integer
(_, sum) <- flip runStateT 0 $ do
for_ [1..n] $ \x -> do
currentSum <- get
lift $ putStrLn ("Current sum is " ++ show currentSum)
modify $ \s -> s + x
putStrLn ("Result is " ++ sum)Помимо комбинаторов runState/runStateT есть аналогичные комбинаторы runReader/runReaderT (из модуля Control.Monad.Reader), предоставляющие неизменяемую ячейку, считать значение которой можно действием ask.
Для многопоточных приложений приходится использовать более сложные виды ячеек. Чаще всего встречаются IORef, MVar и TVar. В частности, о первой из них пойдёт речь в главе, посвящённой написанию веб-сервера.
Де факто стандартной для языка Haskell реализацией массивов являются модули, предоставляемые пакетом vector. У этого пакета довольно сложная структура, требующая пояснений.
Массивы разделяются на два класса:
Data.Vector.GenericData.Vector.Generic.MutableОба этих модуля следует импортировать в qualified-режиме. Перечислим важнейшие операции над неизменяемыми массивами:
generate n f создаёт массив длины n с элементами f(0), f(1), … f(n-1)length v – длина массива vv ! i – элемент массива v с номером iv // [(i1,x1),(i2,x2) ...] ставит на места i1, i2, … значения x1, x2, …thaw v – изменяемая копия массива vОперации над изменяемыми массивами имеют другие названия:
new n создаёт массив длины n, заполненный чем повезётlength v – длина массива vread v i – элемент массива v с номером iwrite v i x – запись по номеру i значения x; modify v i f – изменение элемента с номером i при помощи функции f; swap v i j – обмен элементов с номерами i и jfreeze v – неизменяемая копия массива v; операция определена в модуле Data.Vector.GenericЕстественно, это далеко не все операции. Полный их список следует смотреть в документации.
Все эти операции работают с представителями классов Vector и MVector. Конкретные представители этих классов нужно импортировать из соответствующих модулей. Перечислим наиболее важные типы:
Vector a (модуль Data.Vector) – неизменяемый массив элементов произвольного типаIOVector a (модуль Data.Vector.Mutable) – изменяемый массив элементов произвольного типаVector a (модуль Data.Vector.Unboxed) – неизменяемый массив с элементами одного из базовых типов (например Int, Float и т.д.)IOVector a (модуль Data.Vector.Unboxed.Mutable) – изменяемый массив с элементами одного из базовых типов (например Int, Float и т.д.)Приведём пример программы, считывающей числа в массив и выполняющей сортировку пузырьком:
// сначала на C++
void bubbleSort(vector<int> &v) {
bool changed = true;
while (changed) {
changed = false;
for (unsigned i = 1; i != v.size(); i++) {
if (v[i-1] > v[i]) {
swap(v[i-1],v[i]);
changed = true;
}
}
}
}
int main() {
int n;
cin >> n;
vector<int> numbers(n);
for (unsigned i = 0; i != numbers.size(); i++) {
cin >> numbers[i];
}
bubbleSort(numbers);
for (unsigned i = 0; i != numbers.size(); i++) {
cout << numbers[i] << ' ';
}
cout << endl;
}Теперь её почти дословный перевод на Haskell (изменён только ввод данных):
{-# LANGUAGE FlexibleContexts #-}
import Control.Monad.State.Strict
import Control.Monad.Trans
import Data.Foldable
import Data.Vector.Unboxed(Vector)
import Data.Vector.Unboxed.Mutable(IOVector)
import qualified Data.Vector.Generic as V
import qualified Data.Vector.Generic.Mutable as M
whileState action = do
s <- get
when s (action >> whileState action)
bubbleSort numbers = flip runStateT True $
whileState $ do
put False
for_ [1 .. M.length numbers - 1] $ \i -> do
x <- lift $ M.read numbers (i-1)
y <- lift $ M.read numbers i
when (x > y) $ do
lift $ M.swap numbers (i-1) i
put True
main = do
text <- getContents
let n:rest = read <$> words text :: [Int]
xs = take n rest
numbers <- V.thaw $ (V.fromList xs :: Vector Int)
bubbleSort (numbers :: IOVector Int)
for_ [0 .. M.length numbers - 1] $ \i -> do
x <- M.read numbers i
putStr $ show x ++ " "
putStrLn ""В РАЗРАБОТКЕ
@ 2016 arbrk1, all rights reversed