Uważam to za półprawdę. Haskell ma niesamowitą zdolność abstrakcji, która obejmuje abstrakcje nad imperatywnymi ideami. Na przykład Haskell nie ma wbudowanego imperatywu while, ale możemy go po prostu napisać i teraz robi:
while :: (Monad m) => m Bool -> m () -> m ()
while cond action = do
c <- cond
if c
then action >> while cond action
else return ()
Ten poziom abstrakcji jest trudny dla wielu języków imperatywnych. Można to zrobić w językach imperatywnych, które mają zamknięcia; na przykład. Python i C #.
Ale Haskell ma również (wysoce unikalną) zdolność do charakteryzowania dozwolonych skutków ubocznych za pomocą klas Monad. Na przykład, jeśli mamy funkcję:
foo :: (MonadWriter [String] m) => m Int
Może to być funkcja „imperatywna”, ale wiemy, że może zrobić tylko dwie rzeczy:
- „Wyprowadza” strumień ciągów
- zwraca Int
Nie może drukować na konsoli ani nawiązywać połączeń sieciowych itp. W połączeniu ze zdolnością do abstrakcji można pisać funkcje działające na „dowolne obliczenia, które generują strumień” itp.
Tak naprawdę chodzi o zdolności abstrakcyjne Haskella, co czyni go bardzo dobrym językiem imperatywnym.
Jednak fałszywą połową jest składnia. Uważam, że Haskell jest dość rozwlekły i niezręczny w użyciu w imperatywnym stylu. Oto przykład obliczenia w trybie rozkazującym przy użyciu powyższej while
pętli, które znajduje ostatni element połączonej listy:
lastElt :: [a] -> IO a
lastElt [] = fail "Empty list!!"
lastElt xs = do
lst <- newIORef xs
ret <- newIORef (head xs)
while (not . null <$> readIORef lst) $ do
(x:xs) <- readIORef lst
writeIORef lst xs
writeIORef ret x
readIORef ret
Całe to śmieci IORef, podwójny odczyt, konieczność wiązania wyniku odczytu, fmapping ( <$>
), aby operować na wyniku obliczeń wbudowanych ... to wszystko jest po prostu bardzo skomplikowane. Z funkcjonalnego punktu widzenia ma to wiele sensu , ale języki imperatywne mają tendencję do zamiatania większości tych szczegółów pod dywan, aby ułatwić ich użycie.
Trzeba przyznać, że być może, gdybyśmy użyli while
kombinatora o innym stylu, byłoby to czystsze. Ale jeśli podejmiesz tę filozofię wystarczająco daleko (używając bogatego zestawu kombinatorów, aby wyrazić siebie jasno), to ponownie dojdziesz do programowania funkcjonalnego. Haskell w stylu rozkazującym po prostu nie „płynie” jak dobrze zaprojektowany język imperatywny, np. Python.
Podsumowując, dzięki syntaktycznemu liftingowi twarzy Haskell może być najlepszym językiem imperatywnym. Ale z natury liftingu twarzy byłoby to zastąpienie czegoś wewnętrznie pięknego i prawdziwego czymś zewnętrznie pięknym i fałszywym.
EDYCJA : Porównaj lastElt
z tą transliteracją Pythona:
def last_elt(xs):
assert xs, "Empty list!!"
lst = xs
ret = xs.head
while lst:
ret = lst.head
lst = lst.tail
return ret
Ta sama liczba linii, ale każda linia ma trochę mniej szumów.
EDYCJA 2
Cóż to jest warte, tak wygląda czysty zamiennik w Haskell:
lastElt = return . last
Otóż to. Lub, jeśli zabronisz mi używania Prelude.last
:
lastElt [] = fail "Unsafe lastElt called on empty list"
lastElt [x] = return x
lastElt (_:xs) = lastElt xs
Lub, jeśli chcesz, aby działał na dowolnej Foldable
strukturze danych i zdajesz sobie sprawę, że w rzeczywistości nie musisz IO
obsługiwać błędów:
import Data.Foldable (Foldable, foldMap)
import Data.Monoid (Monoid(..), Last(..))
lastElt :: (Foldable t) => t a -> Maybe a
lastElt = getLast . foldMap (Last . Just)
z Map
, na przykład:
λ➔ let example = fromList [(10, "spam"), (50, "eggs"), (20, "ham")] :: Map Int String
λ➔ lastElt example
Just "eggs"
(.)
Operator złożenie funkcji .