Императивный Haskell
Перевод статьи - Imperative Haskell
Автор - Vaibhav Sagar
Источник оригинальной статьи:
https://vaibhavsagar.com/blog/2017/05/29/imperative-haskell/
Этот пост покрывает по существу тот же самый материал как 5-минутное представление, которое я дал в RC, потому что давать этот доклад снова и снова не масштабируется, и есть вещи, которые я хотел бы охватить, которые трудно в течение этого срока.
Я работал над Тим Roughgarden по Алгоритмам 1 (который теперь был заменен на два меньших курсов) и пытается делать все упражнения в haskell, когда я столкнулся с неприятным фактом. "Quicksort" Хаскелла
qsort [] = []
qsort (x:xs) = lt ++ [x] ++ gt
where lt = qsort [e | e <- xs, e < x]
gt = qsort [e | e <- xs, e >= x]
это не настоящий quicksort! В частности, он не сортирует элементы на месте, и задание, над которым я работал, включало подсчет количества сравнений, поэтому я не мог уйти с моим поддельным quicksort. С моим хвостом между ног я отказался от своего чистого подхода Хаскелла и реализовал решение на Python
import sys
sys.setrecursionlimit(10000)
def partition_first(array, l, r):
p = array[l]
i = l + 1
for j in range(l+1, r):
if array[j] < p:
array[j], array[i] = array[i], array[j]
i += 1
array[l], array[i-1] = array[i-1], array[l]
return (i-1)
def partition_last(array, l, r):
array[r-1], array[l] = array[l], array[r-1]
return partition_first(array, l, r)
def partition_median(array, l, r):
p_idx = choose_median(array, l, r)
array[p_idx], array[l] = array[l], array[p_idx]
return partition_first(array, l, r)
def choose_median(array, l, r):
head = array[l]
last = array[r-1]
length = r-l
if length % 2 == 0:
mid_idx = l + (length//2) - 1
else:
mid_idx = l + (length//2)
mid = array[mid_idx]
options = [(l, head), (mid_idx, mid), (r-1, last)]
options.remove(max(options, key=lambda v: v[1]))
options.remove(min(options, key=lambda v: v[1]))
return options[0][0]
def quicksort(array, start, end, partition):
global comparisons
if end<=start: return
else:
p_idx = partition(array, start, end)
comparisons += (end-start-1)
quicksort(array, start, p_idx, partition)
quicksort(array, p_idx+1, end, partition)
comparisons = 0
inp1 = contents.copy()
quicksort(inp1, 0, len(inp1), partition_first)
print(comparisons)
comparisons = 0
inp2 = contents.copy()
quicksort(inp2, 0, len(inp2), partition_last)
print(comparisons)
comparisons = 0
inp3 = contents.copy()
quicksort(inp3, 0, len(inp3), partition_median)
print(comparisons)
Эта реализация не является особенно Pythonic: обратите внимание на предел рекурсии и использование глобальной переменной. Я фактически забыл сбросить переменную до 0 между итерациями, что было забавно отслеживать. Но это работает!
До сих пор, так хорошо. Это не то, что мы могли бы сделать в Хаскелле, верно? И даже если бы мы могли, эквивалентная реализация была бы настолько другой, чтобы быть неузнаваемой. По крайней мере, это то, что я думал, пока не присмотрелся к Control.Monad.ST и Data.STRef.
Одна из моих самых больших проблем с Haskell-это качество документации. Control.Monad.ST
вводится как
Эта библиотека предоставляет поддержку для строгого государственного потоков, как описано в PLDI ’94 работе Джон Лаунчбюри и Саймон Пейтон Джонс Ленивый Функционального Состояния Потоков.
и Data.STRef
вводится как
Изменяемые ссылки в (строгий) ST монад.
Я не хочу читать статью, чтобы понять, как использовать эти библиотеки, и на самом деле я не должен! Признавая это, я хочу представить альтернативные описания Control.Monad.ST
:
Вы просили изменить состояние, вот оно!
и Data.STRef
:
Переменные, которые вы можете действительно варьировать!!!1!1!один!1eleventyone
Код, использующий эти библиотеки, может показаться очень знакомым людям, привыкшим к императивным языкам, например, к прошлому. Вот выше quicksort переписан в Haskell
{-# LANGUAGE RankNTypes #-}
import Control.Monad.ST
import Data.STRef
import Data.Vector (fromList, toList, freeze, thaw)
import Control.Monad
import Data.Vector.Mutable (STVector, read, write, swap)
import qualified Data.Vector as V (Vector, length)
import Data.List (sortOn)
import Prelude hiding (read)
vector = fromList contents
partitionFirst array l r = do
p <- read array l
i <- newSTRef (l+1)
forM_ [l+1..(r-1)] $ \j -> do
arrayJ <- read array j
i' <- readSTRef i
when (arrayJ < p) $ do
swap array i' j
modifySTRef' i (+1)
i' <- readSTRef i
swap array (i'-1) l
return (i'-1)
partitionLast array l r = do
swap array (r-1) l
partitionFirst array l r
partitionMedian array l r = do
p <- chooseMedian array l r
swap array p l
partitionFirst array l r
chooseMedian array l r = do
h <- read array l
t <- read array (r-1)
let len = r-l
let mid = if (len `mod` 2) == 0
then l + (len `div` 2) - 1
else l + (len `div` 2)
m <- read array mid
let options = sortOn snd [(l, h), (mid, m), (r-1, t)]
return (fst (options !! 1))
quicksort array start end partition comparisons = when (start < end) $ do
i <- partition array start end
modifySTRef' comparisons (+ (end-start-1))
quicksort array start i partition comparisons
quicksort array (i+1) end partition comparisons
quicksort' :: Ord a => V.Vector a -> (forall s a. (Ord a) => STVector s a -> Int -> Int -> ST s Int) -> Int
quicksort' vector partition = runST $ do
array <- thaw vector
comps <- newSTRef 0
quicksort array 0 (V.length vector) partition comps
readSTRef comps
quicksort' vector partitionFirst
quicksort' vector partitionLast
quicksort' vector partitionMedian
Это примерно та же длина, что и реализация Python, и даже улучшает ее в некотором роде: нет предела рекурсии и нет глобальных переменных.
Если мы можем написать Haskell, похожий на Python, и Python является исполняемым псевдокодом, можем ли мы вырезать посредника и перевести псевдокод непосредственно в Haskell? Давайте посмотрим на другую проблему.
Мне нужно было рассчитать Размер сильно связных компонент графа для другого назначения, и я решил использовать алгоритм сильно связных компонентов Тарьяна. Псевдокод для этого (как взято из Википедии) является
algorithm tarjan is
input: graph G = (V, E)
output: set of strongly connected components (sets of vertices)
index := 0
S := empty array
for each v in V do
if (v.index is undefined) then
strongconnect(v)
end if
end for
function strongconnect(v)
// Set the depth index for v to the smallest unused index
v.index := index
v.lowlink := index
index := index + 1
S.push(v)
v.onStack := true
// Consider successors of v
for each (v, w) in E do
if (w.index is undefined) then
// Successor w has not yet been visited; recurse on it
strongconnect(w)
v.lowlink := min(v.lowlink, w.lowlink)
else if (w.onStack) then
// Successor w is in stack S and hence in the current SCC
// Note: The next line may look odd - but is correct.
// It says w.index not w.lowlink; that is deliberate and from the original paper
v.lowlink := min(v.lowlink, w.index)
end if
end for
// If v is a root node, pop the stack and generate an SCC
if (v.lowlink = v.index) then
start a new strongly connected component
repeat
w := S.pop()
w.onStack := false
add w to current strongly connected component
while (w != v)
output the current strongly connected component
end if
end function
и вот как это выглядит в Haskell
import qualified Data.Array as A
import qualified Data.Graph as G
import Control.Monad (forM_, when)
import Control.Monad.ST
import Data.STRef
import Data.Vector.Mutable (STVector, read, replicate, write)
import Prelude hiding (read, replicate)
tarjan graph = runST $ do
index <- newSTRef 0
stack <- newSTRef []
stackSet <- replicate size False
indices <- replicate size Nothing
lowlinks <- replicate size Nothing
output <- newSTRef []
forM_ (G.vertices graph) $ \v -> do
vIndex <- read indices v
when (vIndex == Nothing) $
strongConnect v graph index stack stackSet indices lowlinks output
reverse <$> readSTRef output
where size = snd (A.bounds graph) + 1
strongConnect v graph index stack stackSet indices lowlinks output = do
i <- readSTRef index
write indices v (Just i)
write lowlinks v (Just i)
modifySTRef' index (+1)
push v
forM_ (graph A.! v) $ \w -> read indices w >>= \found -> case found of
Nothing -> do
strongConnect w graph index stack stackSet indices lowlinks output
write lowlinks v =<< (min <$> read lowlinks v <*> read lowlinks w)
Just{} -> read stackSet w >>= \wOnStack -> when wOnStack $
write lowlinks v =<< (min <$> read lowlinks v <*> read indices w)
vLowLink <- read lowlinks v
vIndex <- read indices v
when (vLowLink == vIndex) $ modifySTRef' output . (:) =<< addSCC v []
where
addSCC v scc = do
w <- pop
let scc' = w:scc
if w == v then return scc' else addSCC v scc'
push e = do
modifySTRef' stack (e:)
write stackSet e True
pop = do
e <- head <$> readSTRef stack
modifySTRef' stack tail
write stackSet e False
return e
Помимо явного объявления наших переменных и передачи их, я думаю, что это выглядит довольно близко.
Как это можно объяснить репутацией Хаскелла в чистоте и прозрачности? Вот предмет в статье упомянуто выше , что вам не придется читать (но можешь, если захочешь)! Они придумали способ обеспечения принципиального чистого интерфейса для изменяемого состояния, передав ссылки в качестве аргументов в каждую функцию, которая использует их и использует систему типов, чтобы убедиться, что любая примесь хорошо содержится. Правильность такого подхода была недавно проверена. При желании мы можем заменить любую из функций более чистыми и более идиоматическими определениями, не меняя вывод, и это удовлетворяет определению ссылочной прозрачности!
Почему бы нам не делать это все время, когда Хаскелл, по крайней мере, исправный императивный язык? Потому что писать императивные программы трудно! Они также не составляют, имеют менее полезные сигнатуры типов и их труднее рассуждать. Уйти от этих вещей-вот почему мы должны Haskell начать с! Реальный вопрос должен заключаться в следующем: как мы можем избежать того, чтобы делать это как можно больше?
Прежде чем я открыл эту часть Haskell, я имел это восприятие Haskell (и декларативного программирования в более общем плане) как” императивное программирование, но менее " с практической точки зрения. Я думал, что хотя написание декларативного кода на Python было чисто вопросом дисциплины, написание императивного кода в Haskell требовало полного согласования алгоритма. Благодаря ST
, Я теперь знаю, что это не тот случай, который является огромным облегчением. Если требуется, я могу сделать буквальный перевод алгоритма и очистить его (или нет) позже. На самом деле Haskell-это "императивное программирование и многое другое", и это потрясающе!