Haskell (때때로)이 "Best Imperative Language"로 불리는 이유는 무엇입니까?


84

(이 질문이 주제와 관련이 있기를 바랍니다. 답변을 검색했지만 확실한 답변을 찾지 못했습니다. 주제에서 벗어 났거나 이미 답변 된 경우 검토 / 삭제하십시오.)

Haskell이 최고의 명령형 언어 라는 반 농담을 몇 번 듣고 읽었던 기억이 있습니다 . 물론 Haskell이 일반적으로 기능적 특징 으로 가장 잘 알려져 있기 때문에 이상하게 들립니다 .

그래서 제 질문은 Haskell의 어떤 특성 / 특징이 (있는 경우) Haskell이 최고의 명령형 언어 로 간주되는 이유를 제시하는 것 입니다. 아니면 실제로 농담에 가깝습니까?


3
donsbot.wordpress.com/2007/03/10/… <-프로그래밍 가능한 세미콜론.
vivian 2011

11
이 인용문 은 어색한 분대 를 처리하는 소개의 끝에서 유래했을 것입니다 : "요컨대, Haskell은 세계에서 가장 명령적인 프로그래밍 언어입니다"라고 말하는 Haskell의 모나 딕 입력 / 출력, 동시성, 예외 및 외국어 호출 .
Russell O'Connor

@Russel : 그 말의 가장 가능성있는 출처 (SPJ 자신이되는 것)를 지적 해 주셔서 감사합니다!
hvr 2011-07-10

당신은 하스켈과 엄격한 필수적 각종 작업을 수행 할 수 있습니다 ekmett / 구조체를
야누스 Troelsen에게

답변:


92

나는 그것을 반 진실이라고 생각합니다. Haskell은 추상화하는 놀라운 능력을 가지고 있으며, 여기에는 명령형 아이디어에 대한 추상화가 포함됩니다. 예를 들어, Haskell에는 명령형 while 루프가 내장되어 있지 않지만 작성 만하면됩니다.

while :: (Monad m) => m Bool -> m () -> m ()
while cond action = do
    c <- cond
    if c 
        then action >> while cond action
        else return ()

이 수준의 추상화는 많은 명령형 언어에서 어렵습니다. 이것은 클로저가있는 명령형 언어로 수행 할 수 있습니다. 예. Python 및 C #.

그러나 Haskell 은 Monad 클래스를 사용하여 허용되는 부작용특성화 하는 (매우 고유 한) 능력도 있습니다. 예를 들어, 함수가있는 경우 :

foo :: (MonadWriter [String] m) => m Int

이것은 "명령 적"함수일 수 있지만 두 가지만 수행 할 수 있다는 것을 알고 있습니다.

  • 문자열 스트림 "출력"
  • Int 반환

콘솔에 인쇄하거나 네트워크 연결 등을 설정할 수 없습니다. 추상화 기능과 결합하여 "스트림을 생성하는 모든 계산"등에 작용하는 함수를 작성할 수 있습니다.

매우 훌륭한 명령형 언어로 만드는 것은 Haskell의 추상화 능력에 관한 것입니다.

그러나 거짓 절반은 구문입니다. 나는 Haskell을 명령형 스타일로 사용하는 것이 매우 장황하고 어색하다고 생각합니다. 다음은 while연결 목록의 마지막 요소를 찾는 위의 루프를 사용한 명령형 계산의 예입니다 .

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

모든 IORef 가비지, 이중 읽기, 읽기 결과 바인딩, fmapping ( <$>) 인라인 계산 결과 작업 ... 모두보기가 매우 복잡합니다. 기능 적인 관점에서 보면 상당히 의미가 있지만 명령형 언어는 이러한 세부 사항의 대부분을 사용하기 쉽게 만들기 위해 깔개 아래로 훑어 보는 경향이 있습니다.

물론 우리가 다른 while스타일의 결합자를 사용한다면 더 깨끗할 것입니다. 그러나 그 철학을 충분히 이해한다면 (자신을 명확하게 표현하기 위해 풍부한 조합자를 사용하여), 다시 함수형 프로그래밍에 도달하게됩니다. 명령형 하스켈은 잘 설계된 명령형 언어, 예를 들어 파이썬처럼 "흐르지"않습니다.

결론적으로, 통사론 적 변형을 통해 Haskell은 최고의 명령형 언어 일 수 있습니다. 그러나 페이스 리프트의 특성상 내부적으로 아름답고 실제적인 것을 외부 적으로 아름답고 가짜로 대체하는 것입니다.

편집 : lastElt이 파이썬 음역과 대조 :

def last_elt(xs):
    assert xs, "Empty list!!"
    lst = xs
    ret = xs.head
    while lst:
        ret = lst.head
        lst = lst.tail
    return ret 

라인 수는 같지만 각 라인에는 노이즈가 상당히 적습니다.


2 편집

그만한 가치 는 Haskell 의 순수한 대체품 이 어떻게 생겼는지입니다.

lastElt = return . last

그게 다야. 또는 다음을 사용하는 것을 금지하는 경우 Prelude.last:

lastElt [] = fail "Unsafe lastElt called on empty list"
lastElt [x] = return x
lastElt (_:xs) = lastElt xs

또는 Foldable데이터 구조 에서 작동하고 실제로 오류를 처리 할 필요 가 없다는 것을 인식하려면 다음을 수행하십시오 IO.

import Data.Foldable (Foldable, foldMap)
import Data.Monoid (Monoid(..), Last(..))

lastElt :: (Foldable t) => t a -> Maybe a
lastElt = getLast . foldMap (Last . Just)

와 함께 Map, 예 :

λ➔ let example = fromList [(10, "spam"), (50, "eggs"), (20, "ham")] :: Map Int String
λ➔ lastElt example
Just "eggs"

(.)연산자는 함수 조성물 .


2
더 많은 추상화를 만들어 IORef 노이즈를 훨씬 덜 성가 시게 만들 수 있습니다.
augustss

1
@augustss, 흠, 그것에 대해 궁금합니다. 더 많은 영역 수준의 추상화를 의미합니까, 아니면 더 풍부한 명령형 하위 언어를 구축하는 것입니까? "전자의 경우 동의합니다. . 내가 생각할 수 없기 때문에 기능에 수렴은) 후자의 경우, 난 정말 당신이 무슨 뜻인지보고 싶네 어떻게 내 머리 위로 떨어져.
luqui

2
@luqui ST를 사용하는 것은 허용되는 부작용을 특성화하는 좋은 예입니다. 보너스로 ST에서 순수한 계산으로 돌아갈 수 있습니다.
fuz 2011-07-08

5
파이썬을 비교로 사용하는 것은 완전히 공정하지 않습니다. 여러분이 말했듯이 잘 설계되었으며, 제가 익숙한 구문 적으로 가장 깨끗한 명령형 언어 중 하나입니다. 동일한 비교는 대부분의 명령형 언어가 명령형 스타일로 사용하기 어색하다고 주장 할 것입니다 ...하지만 아마도 그것은 정확히 당신이 의미 한 바일 것입니다. ;]
CA McCann 2011

5
대화에 대한 각주, 후손을 위해 : @augustss는 임시 다형성을 사용하여 IORefs를 암시 적 으로 만들 거나 적어도 GHC 변경을 시도하고이를 방해합니다. : [
CA 맥캔

22

농담이 아니라 믿습니다. 나는 Haskell을 모르는 사람들이 이것을 접근 가능하게 유지하려고 노력할 것입니다. Haskell은 명령형 코드를 작성할 수 있도록 (무엇보다도) do-notation을 사용합니다 (예, 모나드를 사용하지만 걱정하지 마십시오). Haskell이 제공하는 몇 가지 이점은 다음과 같습니다.

  • 간편한 서브 루틴 생성. stdout 및 stderr에 값을 인쇄하는 함수를 원한다고 가정 해 보겠습니다. 다음과 같이 짧은 줄로 서브 루틴을 정의 할 수 있습니다.

    do let printBoth s = putStrLn s >> hPutStrLn stderr s
       printBoth "Hello"
       -- Some other code
       printBoth "Goodbye"
    
  • 코드를 쉽게 전달할 수 있습니다. 위의 내용을 작성 했으므로 이제이 printBoth함수 를 사용하여 모든 문자열 목록을 인쇄하려면 내 서브 루틴을 mapM_함수 에 전달하면 됩니다.

    mapM_ printBoth ["Hello", "World!"]
    

    필수는 아니지만 또 다른 예는 정렬입니다. 길이로만 문자열을 정렬하고 싶다고 가정 해 보겠습니다. 당신은 쓸 수 있습니다:

    sortBy (\a b -> compare (length a) (length b)) ["aaaa", "b", "cc"]
    

    그러면 [ "b", "cc", "aaaa"]가 표시됩니다. (이보다 짧게 쓸 수도 있지만 지금은 신경 쓰지 마십시오.)

  • 재사용하기 쉬운 코드. 이 mapM_함수는 많이 사용되며 다른 언어의 for-each 루프를 대체합니다. 도 있습니다 forever동안 (참) 코드를 통과하고 다른 방법으로 그것을 실행할 수있는 다양한 다른 기능 같은 역할을한다. 따라서 다른 언어의 루프는 Haskell의 이러한 제어 함수로 대체됩니다 (특별하지 않습니다. 매우 쉽게 정의 할 수 있습니다). 일반적으로 이것은 for-each 루프가 긴 반복자 (예 : Java) 또는 배열 인덱싱 루프 (예 : C)보다 잘못되기가 더 어렵 듯이 루프 조건을 잘못 이해하는 것을 어렵게 만듭니다.

  • 바인딩이 할당되지 않았습니다. 기본적으로 하나의 정적 할당처럼 변수에 한 번만 할당 할 수 있습니다. 이것은 주어진 지점에서 변수의 가능한 값에 대한 많은 혼동을 제거합니다 (값은 한 줄에만 설정 됨).
  • 포함 된 부작용. 내가 stdin에서 한 줄을 읽고 그것에 어떤 함수를 적용한 후 stdout에 쓰고 싶다고합시다 (우리는 그것을 foo라고 부를 것입니다). 당신은 쓸 수 있습니다:

    do line <- getLine
       putStrLn (foo line)
    

    foo유형이 String-> String이어야하므로 순수한 함수임을 의미하기 때문에 예상치 못한 부작용 (전역 변수 업데이트, 메모리 할당 해제 등)이 전혀 없다는 것을 즉시 알고 있습니다. 어떤 값을 전달하더라도 부작용없이 매번 동일한 결과를 반환해야합니다. Haskell은 순수 코드에서 부작용 코드를 멋지게 분리합니다. C 또는 Java와 같은 경우에는 이것이 명확하지 않습니다 (getFoo () 메서드가 상태를 변경합니까? 원하지는 않지만 가능할 수도 있습니다 ...).

  • 가비지 수집. 요즘 많은 언어가 가비지 수집되지만 언급 할 가치가 있습니다. 메모리를 할당하고 할당 해제하는 번거 로움이 없습니다.

그 외에도 몇 가지 장점이 더있을 수 있지만, 그 점이 마음에들 것입니다.


9
강력한 타입의 안전성을 추가하겠습니다. Haskell은 컴파일러가 많은 종류의 오류를 제거 할 수 있도록합니다. 최근에 일부 Java 코드를 작업 한 후, 나는 얼마나 끔찍한 널 포인터인지, 그리고 sum 유형없이 얼마나 많은 OOP가 누락되었는지 상기했습니다.
Michael Snoyman 2011

1
정교 해 주셔서 감사합니다! 당신이 언급 한 장점은 하스켈이``필수적 ''효과를 일류 객체 (따라서 결합 가능한)로 취급하는 것과 함께 그 효과를 구분 된 범위에``포함 ''하는 능력으로 귀결되는 것 같습니다. 이것은 적절하게 압축 된 요약입니까?
hvr 2011-07-08

19
@Michael Snoyman :하지만 OOP에서는 합계 유형이 쉽습니다! 데이터 유형의 교회 인코딩을 나타내는 추상 클래스, 케이스에 대한 하위 클래스, 각 케이스를 처리 할 수있는 클래스에 대한 인터페이스를 정의한 다음 제어 흐름에 대한 하위 유형 다형성을 사용하여 각 인터페이스를 지원하는 개체를 합계 개체에 전달하기 만하면됩니다. 당신은). 더 간단 할 수 없습니다. 디자인 패턴을 싫어하는 이유는 무엇입니까?
CA McCann 2011

9
@camccann 나는 당신이 농담하는 것을 알고 있지만 그것은 본질적으로 내 프로젝트에서 구현 한 것입니다.
Michael Snoyman 2011

9
@Michael Snoyman : 좋은 선택입니다! 진짜 농담은 농담처럼 들리는 방식으로 거의 최고의 인코딩이 무엇인지 설명하고 있었다는 것입니다. ㅋ! 교수형까지 웃음 ...
CA McCann

17

다른 사람이 이미 언급 한 것 외에도 부작용이있는 작업을 최고 수준으로 만드는 것이 때때로 유용합니다. 아이디어를 보여주는 어리석은 예가 있습니다.

f = sequence_ (reverse [print 1, print 2, print 3])

이 예제에서는 부작용 (이 예제에서 print)을 사용하여 계산을 구축 한 다음 실제로 실행하기 전에 데이터 구조에 넣거나 다른 방식으로 조작하는 방법을 보여줍니다.


1
이에 해당하는 자바 스크립트 코드는 다음과 같습니다 call = x => x(); sequence_ = xs => xs.forEach(call) ;print = console.log; f = () => sequence_([()=> print(1), () => print(2), () => print(3)].reverse()). 내가 보는 주요 차이점은 몇 가지 추가 () =>.
Hjulle
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.