(Mam nadzieję, że to pytanie jest na temat - próbowałem poszukać odpowiedzi, ale nie znalazłem ostatecznej odpowiedzi. Jeśli zdarzy się, że jest to nie na temat lub już odpowiedziałem, moderuj / usuń je.)
Pamiętam, że kilka razy słyszałem / czytałem pół-żartobliwy komentarz, że Haskell jest najlepszym językiem imperatywnym , co oczywiście brzmi dziwnie, ponieważ Haskell jest zwykle najbardziej znany ze swoich funkcji .
Więc moje pytanie brzmi: jakie cechy / cechy (jeśli w ogóle) Haskella dają powód, by uzasadnić uznanie go za najlepszy język imperatywny - czy też jest to bardziej żart?
Odpowiedzi:
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:
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 musiszIO
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 .źródło
IORef
s ukryte , a przynajmniej stara się i jest udaremnione przez zmiany GHC. : [To nie jest żart i wierzę w to. Postaram się, aby to było dostępne dla tych, którzy nie znają żadnego Haskella. Haskell używa notacji do (między innymi), aby umożliwić pisanie kodu imperatywnego (tak, używa monad, ale nie martw się o to). Oto kilka zalet, które daje ci Haskell:
Łatwe tworzenie podprogramów. Powiedzmy, że chcę, aby funkcja wypisała wartość na stdout i stderr. Mogę napisać co następuje, definiując podprogram za pomocą jednej krótkiej linii:
do let printBoth s = putStrLn s >> hPutStrLn stderr s printBoth "Hello" -- Some other code printBoth "Goodbye"
Łatwy do przekazania kod. Biorąc pod uwagę, że napisałem powyższe, jeśli teraz chcę użyć
printBoth
funkcji do wydrukowania całej listy ciągów, można to łatwo zrobić, przekazując mój podprogram domapM_
funkcji:mapM_ printBoth ["Hello", "World!"]
Innym przykładem, choć nie jest to konieczne, jest sortowanie. Powiedzmy, że chcesz sortować łańcuchy wyłącznie według długości. Możesz pisać:
sortBy (\a b -> compare (length a) (length b)) ["aaaa", "b", "cc"]
Co da ci ["b", "cc", "aaaa"]. (Możesz też napisać to krócej, ale na razie nieważne).
Łatwy do ponownego użycia kod. Ta
mapM_
funkcja jest często używana i zastępuje pętle for-each w innych językach. Istnieje równieżforever
funkcja, która działa jak while (prawda) i różne inne funkcje, które można przekazać kod i wykonać go na różne sposoby. Więc pętle w innych językach są zastępowane przez te funkcje kontrolne w Haskell (które nie są specjalne - możesz je bardzo łatwo zdefiniować samodzielnie). Ogólnie rzecz biorąc, utrudnia to błędne określenie warunku pętli, podobnie jak pętle for-each trudniej jest popełnić błąd niż odpowiedniki iteratorów z długiej ręki (np. W Javie) lub pętle indeksujące tablice (np. W C).Zawarte skutki uboczne. Powiedzmy, że chcę odczytać linię ze stdin i zapisać ją na stdout po zastosowaniu do niej jakiejś funkcji (nazwiemy to foo). Możesz pisać:
do line <- getLine putStrLn (foo line)
Wiem od razu, że
foo
nie ma to żadnych nieoczekiwanych skutków ubocznych (takich jak aktualizacja zmiennej globalnej, zwolnienie pamięci itp.), Ponieważ musi to być typ String -> String, co oznacza, że jest to czysta funkcja; niezależnie od wartości, którą podam, za każdym razem musi zwracać ten sam wynik, bez skutków ubocznych. Haskell ładnie oddziela kod powodujący efekty uboczne od czystego kodu. W czymś takim jak C, czy nawet Java, nie jest to oczywiste (czy ta metoda getFoo () zmienia stan? Miałbyś nadzieję, że nie, ale może tak ...).Poza tym prawdopodobnie jest jeszcze kilka zalet, ale to są te, które przychodzą na myśl.
źródło
Oprócz tego, o czym wspomnieli już inni, czasami przydatne jest posiadanie pierwszorzędnych działań o skutkach ubocznych. Oto głupi przykład pokazujący pomysł:
f = sequence_ (reverse [print 1, print 2, print 3])
Ten przykład pokazuje, jak można budować obliczenia z efektami ubocznymi (w tym przykładzie
print
), a następnie umieścić je w strukturach danych lub manipulować nimi w inny sposób, zanim faktycznie je wykonasz.źródło
call = x => x(); sequence_ = xs => xs.forEach(call) ;print = console.log; f = () => sequence_([()=> print(1), () => print(2), () => print(3)].reverse())
. Główną różnicą, jaką widzę, jest to, że potrzebujemy kilku dodatkowych() =>
.