실무자로서 왜 Haskell에 관심을 가져야합니까? 모나드는 무엇이며 왜 필요합니까? [닫은]


9

나는 그들이 어떤 문제를 해결하지 못합니다.



2
이 편집은 약간 극단적이라고 생각합니다. 나는 당신의 질문이 본질적으로 좋은 것이라고 생각합니다. 그것은 단지 그것의 일부가 조금 논쟁 적이었던 것입니다. 아마 당신이 보지 못했던 무언가를 배우려는 노력의 좌절의 결과 일 것입니다.
Jason Baker

@ SnOrfus, 나는 그 질문을 나쁜 사람이었습니다. 제대로 편집하기에는 너무 게으르다.
Job

답변:


34

모나드는 좋지도 않습니다. 그들은 단지 있습니다. 그것들은 다른 많은 프로그래밍 언어 구성과 같은 문제를 해결하는 데 사용되는 도구입니다. 그것들의 하나의 매우 중요한 적용은 순전히 기능적인 언어로 작업하는 프로그래머를 위해 인생을 더 쉽게 만드는 것입니다. 그러나 비 기능적 언어에서는 유용합니다. 사람들이 모나드를 사용하고 있다는 것을 거의 인식하지 못하는 것입니다.

모나드는 무엇입니까? Monad를 생각하는 가장 좋은 방법은 디자인 패턴입니다. I / O의 경우 아마도 글로벌 상태가 단계들 사이를 통과하는 영광스러운 파이프 라인 이상이라고 생각할 수 있습니다.

예를 들어, 작성중인 코드를 보자.

do
  putStrLn "What is your name?"
  name <- getLine
  putStrLn ("Nice to meet you, " ++ name ++ "!")

눈을 만나는 것보다 훨씬 더 많은 일이 있습니다. 예를 들어 putStrLn다음과 같은 서명이 putStrLn :: String -> IO ()있습니다. 왜 이런거야?

stdout과 stdin이 읽고 쓸 수있는 유일한 파일이라고 가정 해 봅시다 (간단하게하기 위해). 명령형 언어에서는 이것이 문제가되지 않습니다. 그러나 기능적 언어에서는 전역 상태를 변경할 수 없습니다. 함수는 단순히 값을 가져 와서 값을 반환하는 것입니다. 이를 해결하는 한 가지 방법은 전역 상태를 각 함수로 전달되거나 전달되는 값으로 사용하는 것입니다. 따라서 첫 번째 코드 줄을 다음과 같이 변환 할 수 있습니다.

global_state <- (\(stdin, stdout) -> (stdin, stdout ++ "What is your name?")) global_state

... 그리고 컴파일러는의 두 번째 요소에 추가 된 것을 인쇄하는 것을 알고있을 것입니다 global_state. 이제 나는 당신에 대해 모른다, 그러나 나는 그런 프로그램을 싫어. 이것이 더 쉬운 방법은 Monads를 사용하는 것이 었습니다. Monad에서는 어떤 동작에서 다음 동작으로 어떤 종류의 상태를 나타내는 값을 전달합니다. 이것이 putStrLn반환 유형 이 다음과 같은 이유 입니다 IO (). 새로운 전역 상태를 반환합니다.

근데 왜 신경써? 명령형 프로그램에 비해 함수형 프로그래밍의 장점은 여러 곳에서 논의되어 왔기 때문에 일반적인 질문에 대답하지는 않겠습니다 (그러나 함수형 프로그래밍의 사례를 들으려면 이 백서를 참조하십시오 ). 그러나이 특정한 경우에, Haskell이 달성하려는 것을 이해하면 도움이 될 수 있습니다.

많은 프로그래머들은 Haskell이 명령형 코드를 작성하거나 부작용을 사용하지 못하게하려고한다고 생각합니다. 사실이 아닙니다. 명령 언어는 기본적으로 부작용을 허용하지만 실제로 원하는 경우 기능 코드를 작성할 수있는 언어입니다 (필요한 일부 변형을 기꺼이 처리 할 수 ​​있음). Haskell은 기본적으로 순전히 기능적이지만 실제로 원하는 경우 명령형 코드를 작성할 수 있습니다 (프로그램이 유용한 경우 수행함). 요점은 부작용이있는 코드를 작성하는 것을 어렵게 만드는 것이 아닙니다. 부작용이 있다는 점을 분명히해야합니다 (유형 시스템으로).


6
마지막 문단은 금입니다. "필수 언어는 기본적으로 부작용을 허용하지만 실제로 원하는 경우 기능 코드를 작성할 수있는 언어입니다. 기능 언어는 기본적으로 순수하게 기능하지만 명령 코드를 작성할 수는 있습니다. 정말로 원한다면 "
Frank Shearar

당신이 링크 한 논문 은 "기능적 프로그래밍의 미덕으로서의 불변성 (immutability)"이라는 개념은 처음부터 바로 거부 한다는 점은 주목할 가치가 있습니다.
메이슨 휠러

@MasonWheeler : 나는 불변성의 중요성을 무시하는 것이 아니라 함수형 프로그래밍의 우월성을 입증하기위한 설득력있는 논증 으로 그 단락을 읽었습니다 . 사실, 그는 goto논문에서 조금 후에 (구조화 프로그래밍의 논거로서)를 제거 하고 "과일이없는"과 같은 논증을 특징 짓는 것에 대해서도 같은 말을했다 . 그러나 우리 중 누구도 비밀리에 goto돌아 오기를 원하지 않습니다 . 단순히 goto그것을 광범위하게 사용하는 사람들에게 필요 하지 않다고 주장 할 수 없다는 것 입니다.
Robert Harvey

7

물린거야 !!! 모나드 그 자체는 하스켈에게있어서 실제로 존재하지 않는 것은 아닙니다 (하스켈의 초기 버전에는 없었습니다).

귀하의 질문은 "구문을 볼 때 C ++는 지루해집니다. 그러나 템플릿은 C ++의 보급형 기능이므로 다른 언어로 구현 한 것을 보았습니다"라고 말합니다.

Haskell 프로그래머의 진화는 농담입니다. 진지하게 받아 들여지는 것은 아닙니다.

Haskell에서 프로그램의 목적을위한 Monad는 Monad 유형 클래스의 인스턴스입니다. 즉, 특정 소규모 작업 집합을 지원하는 유형입니다. Haskell은 Monad 유형 클래스를 구현하는 유형, 특히 구문 지원을 특별히 지원합니다. 실제로 이러한 결과는 "프로그램 가능한 세미콜론"이라고합니다. 이 기능을 Haskell의 다른 기능 (기본적으로 일류 함수, 게으름)과 결합하면 언어 기능으로 간주되었던 라이브러리로 특정 항목을 구현할 수 있습니다. 예를 들어 예외 메커니즘을 구현할 수 있습니다. 라이브러리로 연속 및 코 루틴에 대한 지원을 구현할 수 있습니다. Haskell, 언어는 가변 변수를 지원하지 않습니다 :

"아마도 / 신원 / 안전 부문 모나드 ???"에 대해 묻습니다. Maybe 모나드는 라이브러리로 예외 처리를 구현하는 방법에 대한 예입니다.

맞습니다. 메시지를 쓰고 사용자 입력을 읽는 것은 독특하지 않습니다. IO는 "모니터로서의 모나드"의 대표적인 예입니다.

그러나 반복하기 위해 언어의 나머지 부분과 분리 된 하나의 "기능"(예 : Monads) 자체가 즉시 유용한 것으로 보이지는 않습니다 (C ++ 0x의 새로운 기능은 rvalue 참조입니다. 구문이 지루하고 반드시 유틸리티를 볼 수 있기 때문에 컨텍스트 C ++에서 벗어날 수 있습니다. 프로그래밍 언어는 버킷에 많은 기능을 던져서 얻는 것이 아닙니다.


실제로, haskell은 ST 모나드를 통해 가변 변수를 지원합니다 (자체 규칙에 따라 언어의 몇 가지 이상한 불순한 마법 부분 중 하나).
사라

4

프로그래머는 모두 프로그램을 작성하지만 유사점은 끝납니다. 프로그래머는 대부분의 프로그래머가 상상할 수있는 것보다 훨씬 다르다고 생각합니다. 정적 변수 입력 대 런타임 전용 형식, 스크립팅 대 컴파일, C 스타일 대 객체 지향과 같은 오랜 "전투"를 수행하십시오. 한 캠프가 열등하다는 주장을 합리적으로 주장하는 것이 불가능하다는 것을 알게 될 것입니다. 그 중 일부는 나에게 쓸모 없거나 전혀 사용할 수없는 일부 프로그래밍 시스템에서 우수한 코드를 생성하기 때문입니다.

나는 다른 사람들이 다르게 생각한다고 생각합니다. 만약 당신이 구문상의 설탕이나 특히 편의를 위해 존재하고 실제로 런타임 비용이 큰 추상화로 유혹받지 않는다면, 그런 언어를 피하십시오.

그러나 최소한 포기하고있는 개념에 익숙해 지도록 권장합니다. 나는 격렬하게 프로 순수 C 사람에 대하여 아무 것도없는 만큼 그들이 실제로 큰 문제는 람다 식에 관한 이해를. 나는 대부분의 팬이 즉시 팬이되지는 않을 것이라고 생각하지만, 그들이 람다로 해결하기가 훨씬 쉬운 완벽한 문제를 발견 할 때 적어도 마음 뒤에있을 것이다.

그리고 무엇보다도 팬보이의 말, 특히 그들이 무엇을 말하는지 실제로 모르는 사람들에 의해 화를 내지 않도록 노력하십시오.


4

Haskell은 참조 투명성을 강화합니다 . 동일한 매개 변수가 지정되면 해당 함수를 몇 번 호출하더라도 모든 함수는 항상 동일한 결과를 반환합니다.

예를 들어, Haskell (모나드 제외)에서는 난수 생성기를 구현할 수 없습니다. C ++ 또는 Java에서는 전역 변수를 사용하여 랜덤 생성기의 중간 "시드"값을 저장하여이를 수행 할 수 있습니다.

Haskell에서 전역 변수의 대응 물은 Monad입니다.


그렇다면 난수 생성기를 원한다면 어떻게해야할까요? 그것은 기능이 아닌가? 그렇지 않더라도 난수 생성기를 어떻게 얻습니까?
직업

@Job 모나드 (기본적으로 상태 추적기) 내에서 난수 생성기를 만들거나 사용해서는 안되는 Haskell의 악마 unsafePerformIO를 사용할 수 있습니다 (실제로 임의성을 사용하면 프로그램이 중단 될 수 있음) 그것의 내부!)
대안

Haskell에서는 기본적으로 RNG의 현재 상태 인 'RandGen'을 전달합니다. 따라서 새로운 난수를 생성하는 함수는 RandGen을 가져 와서 새로운 RandGen과 생성 된 수를 가진 튜플을 반환합니다. 대안은 최소값과 최대 값 사이의 난수 목록을 원하는 곳에 지정하는 것입니다. 이것은 느리게 평가 된 무한한 수의 스트림을 반환하므로 새로운 난수가 필요할 때마다이 목록을 살펴볼 수 있습니다.
Qqwy

다른 언어로도 같은 방법으로! 의사 난수 생성기 알고리즘을 확보 한 다음 값으로 시드하고 "랜덤"숫자가 표시됩니다! 유일한 차이점은 C # 및 Java와 같은 언어는 시스템 시계 또는 이와 유사한 것을 사용하여 PRNG를 자동으로 제공한다는 것입니다. 사실 그리고 haskell에서는 "다음"번호를 얻는 데 사용할 수있는 새로운 PRNG를 얻는 반면 C # / Java에서는 모두 Random개체의 가변 변수를 사용하여 내부적으로 수행됩니다 .
사라

4

오래된 질문의 종류이지만 정말 좋은 질문이므로 대답하겠습니다.

모나드는 코드 실행 방식을 완벽하게 제어 할 수있는 코드 블록으로 생각할 수 있습니다. 각 코드 행이 반환해야하는 사항, 실행을 중단해야하는지 여부, 각 행 사이에 다른 처리가 발생하는지 여부.

모나드가 가능하게하는 것들에 대한 몇 가지 예를 들겠습니다. 내 Haskell 지식이 약간 흔들 리기 때문에 Haskell에는 이러한 예제가 없지만, Haskell이 모나드 사용에 어떻게 영감을 주 었는지에 대한 예입니다.

파서

일반적으로 어떤 종류의 파서를 작성하고 프로그래밍 언어를 구현하려는 경우 BNF 사양을 읽고 구문 분석을 위해 전체 루프 코드를 작성하거나 컴파일러 컴파일러 를 사용해야합니다 Flex, Bison, yacc 등이 있습니다. 그러나 모나드를 사용하면 Haskell에서 일종의 "컴파일러 파서"를 만들 수 있습니다.

구문 분석기는 모나드 또는 yacc, bison 등과 같은 특수 목적 언어가 없으면 실제로 수행 할 수 없습니다.

예를 들어, IRC 프로토콜에 대한 BNF 언어 사양을 사용했습니다 .

message    =  [ ":" prefix SPACE ] command [ params ] crlf
prefix     =  servername / ( nickname [ [ "!" user ] "@" host ] )
command    =  1*letter / 3digit
params     =  *14( SPACE middle ) [ SPACE ":" trailing ]
           =/ 14( SPACE middle ) [ SPACE [ ":" ] trailing ]

nospcrlfcl =  %x01-09 / %x0B-0C / %x0E-1F / %x21-39 / %x3B-FF
                ; any octet except NUL, CR, LF, " " and ":"
middle     =  nospcrlfcl *( ":" / nospcrlfcl )
trailing   =  *( ":" / " " / nospcrlfcl )

SPACE      =  %x20        ; space character
crlf       =  %x0D %x0A   ; "carriage return" "linefeed"

그리고 F #에서 모나드를 지원하는 또 다른 언어 인 약 40 줄의 코드로 처리했습니다.

type UserIdentifier = { Name : string; User: string; Host: string }

type Message = { Prefix : UserIdentifier option; Command : string; Params : string list }

let space = character (char 0x20)

let parameters =
    let middle = parser {
        let! c = sat <| fun c -> c <> ':' && c <> (char 0x20)
        let! cs = many <| sat ((<>)(char 0x20))
        return (c::cs)
    }
    let trailing = many item
    let parameter = prefixed space ((prefixed (character ':') trailing) +++ middle)
    many parameter

let command = atLeastOne letter +++ (count 3 digit)

let prefix = parser {
    let! name = many <| sat (fun c -> c <> '!' && c <> '@' && c <> (char 0x20))   //this is more lenient than RFC2812 2.3.1
    let! uh = parser {
        let! user = maybe <| prefixed (character '!') (many <| sat (fun c -> c <> '@' && c <> (char 0x20)))
        let! host = maybe <| prefixed (character '@') (many <| sat ((<>) ' '))
        return (user, host)
    }
    let nullstr = function | Some([]) -> null | Some(s) -> charsString s | _ -> null
    return { Name = charsString name; User = nullstr (fst uh); Host = nullstr (snd uh) }
}

let message = parser {
    let! p = maybe (parser {
        let! _ = character ':'
        let! p = prefix
        let! _ = space
        return p
    })
    let! c = command
    let! ps = parameters
    return { Prefix = p; Command = charsString c; Params = List.map charsString ps }
}

F #의 모나드 구문은 Haskell과 비교할 때 매우 추악합니다.이를 상당히 개선했을 수도 있습니다.하지만 구조적으로 파서 코드는 BNF와 동일합니다. 이것은 모나드 (또는 파서 생성기)없이 훨씬 더 많은 작업을 수행했을뿐만 아니라 사양과 거의 유사하지 않았으므로 읽고 유지하는 것이 끔찍했습니다.

맞춤형 멀티 태스킹

일반적으로 멀티 태스킹은 OS 기능으로 생각되지만 모나드를 사용하면 각 명령 모나드 후 프로그램이 스케줄러로 제어권을 넘겨서 다른 모나드를 실행할 수 있도록 자신 만의 스케줄러를 작성할 수 있습니다.

한 사람이 게임 루프를 제어하기 위해 "태스크"모나드 를 만들었습니다. (F #에서 다시 한 번) 모든 Update()호출에서 작동하는 상태 머신으로 모든 것을 작성하는 대신 모든 명령을 마치 단일 함수 인 것처럼 작성할 수 있습니다. .

즉, 다음과 같은 작업을 수행하지 않아도됩니다.

class Robot
{
   enum State { Walking, Shooting, Stopped }

   State state = State.Stopped;

   public void Update()
   {
      switch(state)
      {
         case State.Stopped:
            Walk();
            state = State.Walking;
            break;
         case State.Walking:
            if (enemyInSight)
            {
               Shoot();
               state = State.Shooting;
            }
            break;
      }
   }
}

당신은 다음과 같은 것을 할 수 있습니다 :

let robotActions = task {
   while (not enemyInSight) do
      Walk()
   while (enemyInSight) do
      Shoot()
}

LINQ to SQL

LINQ to SQL은 실제로 모나드의 예이며, 유사한 기능을 Haskell에서 쉽게 구현할 수 있습니다.

나는 모든 것을 정확하게 기억하지 못하기 때문에 세부 사항을 다루지 않지만 Erik Meijer는 그것을 잘 설명합니다 .


1

GoF 패턴에 익숙하다면 모나드는 방사성 오소리에 물린 스테로이드에 데코레이터 패턴과 빌더 패턴을 결합한 것과 같습니다.

위에 더 나은 답변이 있지만 내가 볼 수있는 특정 이점은 다음과 같습니다.

  • 모나드는 코어 유형을 변경하지 않고 추가 속성으로 일부 코어 유형을 장식합니다. 예를 들어, 모나드는 문자열을 "리프트"하고 "isWellFormed", "isProfanity"또는 "isPalindrome"등과 같은 값을 추가 할 수 있습니다.

  • 이와 유사하게 모나드는 간단한 유형을 수집 유형으로 집약 할 수 있습니다.

  • 모나드는이 고차 공간에 함수를 늦게 바인딩 할 수 있습니다

  • 모나드는 상위 공간에서 임의의 함수와 인수를 임의의 데이터 유형과 혼합 할 수 있습니다

  • 모나드는 순수한 상태 비 저장 함수와 불완전한 상태 기반을 혼합하여 문제가 발생한 위치를 추적 할 수 있습니다

Java에서 모나드의 익숙한 예는 List입니다. String과 같은 일부 핵심 클래스를 사용하여 List에 대한 정보를 추가하여 List의 모나드 공간으로 "리프트"합니다. 그런 다음 get (), getFirst (), add (), empty () 등과 같은 새 함수를 해당 공간에 바인딩합니다.

대규모로 프로그램을 작성하는 대신 큰 빌더 (GoF 패턴으로)를 작성했으며 마지막에 build () 메소드가 프로그램이 생성해야하는 모든 응답을 생성했다고 상상해보십시오. 그리고 원래 코드를 다시 컴파일하지 않고도 ProgramBuilder에 새 메서드를 추가 할 수 있습니다. 이것이 모나드가 강력한 디자인 모델 인 이유입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.