| GetColorings Science Blog |

Printable Coloring Pages

Императивный 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-это "императивное программирование и многое другое", и это потрясающе!