Haskell에서 부작용이 모나드로 모델링 된 이유는 무엇입니까?


172

하스켈의 불순한 계산이 모나드로 모델링 된 이유에 대해 몇 가지 조언을 해 줄 수 있습니까?

나는 모나드가 단지 4 개의 연산과의 인터페이스라는 것을 의미한다. 그래서 부작용을 모델링하는 이유는 무엇인가?


15
Monads는 두 가지 작업을 정의합니다.
Dario

3
그러나 반환과 실패는 어떻습니까? ((>>) 및 (>> =) 제외)
bodacydo

55
두 가지 작업은 return(>>=)입니다. x >> y와 동일합니다 x >>= \\_ -> y(즉, 첫 번째 인수의 결과를 무시합니다). 우리는에 대해 이야기하지 않습니다 fail.
poges

2
@Porges 왜 실패에 대해 이야기하지 않습니까? 어쩌면 파서 등에서 다소 유용하다
대안

16
@monadic : failMonad있기 때문에 역사적 사고의 클래스; 정말에 속합니다 MonadPlus. 기본 정의는 안전하지 않습니다.
JB.

답변:


292

함수에 부작용이 있다고 가정하십시오. 입력 및 출력 매개 변수로 생성되는 모든 효과를 취하면 함수는 외부 세계에 순수합니다.

따라서 불순한 기능

f' :: Int -> Int

우리는 고려에 RealWorld를 추가

f :: Int -> RealWorld -> (Int, RealWorld)
-- input some states of the whole world,
-- modify the whole world because of the side effects,
-- then return the new world.

그런 다음 f다시 순수합니다. 매개 변수화 된 데이터 유형을 정의 type IO a = RealWorld -> (a, RealWorld)하므로 RealWorld를 여러 번 입력 할 필요가 없으며 작성 만 가능합니다.

f :: Int -> IO Int

프로그래머에게는 RealWorld를 직접 처리하는 것이 너무 위험합니다. 특히 프로그래머가 RealWorld 유형의 값에 손을 대면 복사를 시도 할 수 있으며 기본적으로 불가능합니다. (예를 들어, 전체 파일 시스템을 복사하려고한다고 생각하십시오. 어디에 두겠습니까?) 따라서 IO에 대한 정의는 전 세계의 상태도 캡슐화합니다.

"불완전한"기능의 구성

이러한 불순한 기능은 서로 연결할 수 없다면 쓸모가 없습니다. 치다

getLine     :: IO String            ~            RealWorld -> (String, RealWorld)
getContents :: String -> IO String  ~  String -> RealWorld -> (String, RealWorld)
putStrLn    :: String -> IO ()      ~  String -> RealWorld -> ((),     RealWorld)

우리는하고 싶다

  • 콘솔에서 파일 이름을 습니다.
  • 그 파일을 읽고
  • 해당 파일의 내용을 콘솔에 인쇄 하십시오.

실제 상태에 접근 할 수 있다면 어떻게해야할까요?

printFile :: RealWorld -> ((), RealWorld)
printFile world0 = let (filename, world1) = getLine world0
                       (contents, world2) = (getContents filename) world1 
                   in  (putStrLn contents) world2 -- results in ((), world3)

여기서 패턴이 보입니다. 함수는 다음과 같이 호출됩니다.

...
(<result-of-f>, worldY) = f               worldX
(<result-of-g>, worldZ) = g <result-of-f> worldY
...

따라서 ~~~바인딩 할 연산자 를 정의 할 수 있습니다.

(~~~) :: (IO b) -> (b -> IO c) -> IO c

(~~~) ::      (RealWorld -> (b,   RealWorld))
      ->                    (b -> RealWorld -> (c, RealWorld))
      ->      (RealWorld                    -> (c, RealWorld))
(f ~~~ g) worldX = let (resF, worldY) = f worldX
                   in g resF worldY

우리는 간단히 쓸 수 있습니다

printFile = getLine ~~~ getContents ~~~ putStrLn

현실 세계를 건드리지 않고

"불순 화"

이제 파일 내용을 대문자로 만들고 싶다고 가정 해 봅시다. 대문자는 순수한 기능입니다

upperCase :: String -> String

그러나 그것을 현실 세계로 만들기 위해서는를 돌려 주어야합니다 IO String. 그러한 기능을 들어 올리는 것은 쉽습니다.

impureUpperCase :: String -> RealWorld -> (String, RealWorld)
impureUpperCase str world = (upperCase str, world)

이것은 일반화 될 수 있습니다 :

impurify :: a -> IO a

impurify :: a -> RealWorld -> (a, RealWorld)
impurify a world = (a, world)

그래서 impureUpperCase = impurify . upperCase, 우리는 쓸 수 있습니다

printUpperCaseFile = 
    getLine ~~~ getContents ~~~ (impurify . upperCase) ~~~ putStrLn

(참고 : 일반적으로 작성 getLine ~~~ getContents ~~~ (putStrLn . upperCase))

우리는 모나드와 함께 일하고있었습니다

이제 우리가 한 일을 봅시다 :

  1. 우리는 (~~~) :: IO b -> (b -> IO c) -> IO c두 개의 불순한 기능을 함께 묶는 연산자 를 정의했습니다.
  2. impurify :: a -> IO a순수한 값을 불순으로 변환 하는 함수 를 정의했습니다 .

이제 우리는 확인하게 (>>=) = (~~~)하고 return = impurify, 볼? 모나드가 있습니다.


기술 노트

그것이 실제로 모나드임을 보장하기 위해 여전히 확인해야 할 몇 가지 공리가 있습니다.

  1. return a >>= f = f a

     impurify a                =  (\world -> (a, world))
    (impurify a ~~~ f) worldX  =  let (resF, worldY) = (\world -> (a, world )) worldX 
                                  in f resF worldY
                               =  let (resF, worldY) =            (a, worldX)       
                                  in f resF worldY
                               =  f a worldX
    
  2. f >>= return = f

    (f ~~~ impurify) worldX  =  let (resF, worldY) = f worldX 
                                in impurify resF worldY
                             =  let (resF, worldY) = f worldX      
                                in (resF, worldY)
                             =  f worldX
    
  3. f >>= (\x -> g x >>= h) = (f >>= g) >>= h

    운동으로 떠났다.


5
+1이지만 IO 사례를 구체적으로 다루고 있습니다. blog.sigfpe.com/2006/08/you-could-have-invented-monads-and.html 은 매우 비슷하지만, 일반화 RealWorld되어 있습니다.
ephemient

4
IO후자는 상호 작용, 동시성 및 비결 정성을 지원하기 때문에이 설명은 Haskell에 실제로 적용 할 수 없습니다 . 더 많은 포인터에 대해서는이 질문에 대한 내 대답을 참조하십시오.
Conal

2
@Conal GHC는 실제로 IO이런 식으로 구현 하지만 RealWorld실제로는 실제를 나타내는 것이 아니라 작업을 순서대로 유지하는 토큰 일뿐입니다 ( "매직" RealWorld은 GHC Haskell의 유일한 고유성 유형 임)
Jeremy List

2
@JeremyList 내가 이해하는 것처럼 GHC IO는이 표현과 비표준 컴파일러 마술 ( Ken Thompson의 유명한 C 컴파일러 바이러스를 연상시키는)을 조합하여 구현 합니다. 다른 유형의 경우 진실은 일반적인 Haskell 의미와 함께 소스 코드에 있습니다.
Conal

1
@Clonal 내 의견은 GHC 소스 코드의 관련 부분을 읽었 기 때문입니다.
제레미 목록

43

하스켈의 불순한 계산이 모나드로 모델링 된 이유에 대해 누군가가 조언을 해 줄 수 있습니까?

이 질문에는 광범위한 오해가 포함되어 있습니다. 불순물과 Monad는 독립적 인 개념입니다. 불순물은 Monad에 의해 모델링 되지 않았습니다 . 대신, IO명령형 계산을 나타내는와 같은 몇 가지 데이터 유형이 있습니다 . 이러한 유형 중 일부의 경우 인터페이스의 작은 부분이 "Monad"라는 인터페이스 패턴에 해당합니다. 더욱이, 순수한 / 기능적 / 지표 적 설명은 알려져 있지 않으며 IO( "sin bin"의 목적을 고려하여 하나도 없을 것임 IO), World -> (a, World)의 의미에 대한 일반적인 이야기 가 IO a있습니다. 이야기는 정직하게 설명 할 수 IO있기 때문에IO동시성과 비결 정성을 지원합니다. 이야기는 세상과의 중간 계산 상호 작용을 허용하는 결정 론적 계산에 대해서는 효과가 없습니다.

자세한 설명은 이 답변을 참조하십시오 .

편집 : 질문을 다시 읽을 때 내 대답이 제대로 진행되고 있다고 생각하지 않습니다. 명령형 계산 모델은 종종 질문이 말한 것처럼 모나드로 판명됩니다. asker는 모나드가 어떤 식 으로든 명령형 계산의 모델링을 가능하게한다고 가정하지 않을 수도 있습니다.


1
@ KennyTM :하지만 RealWorld문서에서 말하는 것처럼 "매우 마 법적"입니다. 런타임 시스템이 수행하는 작업을 나타내는 토큰이며 실제로 실제 세계에 대한 의미는 아닙니다. 추가 속임수없이 "스레드"를 만들기 위해 새로운 것을 만들 수도 없습니다. 순진한 접근 방식은 언제 실행 될지에 대해 많은 모호함을 가진 단일의 차단 동작을 생성합니다.
CA McCann

4
또한 모나드 본질적으로 필수적 이라고 주장합니다 . functor에 값이 포함 된 일부 구조를 나타내는 경우 모나드 인스턴스는 해당 값을 기반으로 새 레이어를 작성하고 평면화 할 수 있음을 의미합니다. 따라서 functor의 단일 레이어에 할당하는 의미가 무엇이든 모나드는 엄격한 인과 관계 개념을 사용하여 무한한 수의 레이어를 만들 수 있음을 의미합니다. 특정 사례는 본질적으로 명령적인 구조를 갖지 않을 수도 있지만 Monad일반적으로 실제로 그러합니다.
CA McCann

3
" Monad일반적으로"는 대략 forall m. Monad m => ...임의의 인스턴스에서 작업하는 것을 의미 합니다. 임의 모나드로 수행 할 수있는 작업은 수행 할 수있는 작업과 거의 동일합니다 IO. (불러 진 프리미티브 (각각 함수 인수 또는 라이브러리에서) 수신,로 no-ops 구성 return또는 되돌릴 수없는 방식으로 값 변환) (>>=). 임의 모나드에서의 프로그래밍의 본질은 취소 할 수없는 행동의 목록을 생성하는 것입니다 : "X를 수행 한 다음 Y를 수행 한 다음 ...". 나에게 꼭 필요한 소리!
CA McCann

2
아니, 여전히 내 요점이 없어 물론 명확하고 의미있는 구조를 가지고 있기 때문에 특정 유형에 대해서는 이러한 사고 방식을 사용하지 않을 것입니다. "임의 모나드"라고 말하면 "어느 쪽을 선택하지 않아도됩니다"라는 의미입니다. 여기서의 관점은 정량화 기 내부에서 온 것이므로 m실존 적이 라고 생각하는 것이 더 도움이 될 수 있습니다. 또한, 내 "해석"는 것입니다 다른 표현 법칙은; "do X"문장의 목록은 정확히 통해 알려지지 않은 구조에 대한 자유 모노 이드이다 (>>=); 모나드 법칙은 endofunctor 구성에 대한 독점 법입니다.
CA McCann

3
요컨대, 모든 모나드가 함께 설명하는 가장 큰 하한은 장님, 의미없는 미래로의 행진입니다. 이 최소값 이상을 거의IO 제공 하지 않기 때문에 병리학 적 사례 입니다. 특정한 경우 유형은 더 많은 구조를 나타내므로 실제 의미를 가질 수 있습니다. 그러나 법에 근거한 모나드의 본질적인 속성은 그대로 표기를 명확하게하는 데 반신 도적 IO입니다. 생성자를 내보내거나 원시 동작 또는 이와 유사한 것을 철저히 열거하지 않으면 상황은 절망적입니다.
CA McCann

13

내가 이해할 때, Eugenio Moggi 라는 사람은 처음에는 "모 노드"라고 불리는 이전에 모호한 수학적 구성이 컴퓨터 언어로 부작용을 모델링하는 데 사용될 수 있다는 것을 알아 차렸다. 따라서 Lambda 미적분을 사용하여 의미를 지정한다. Haskell이 개발 될 때 불완전한 계산이 모델링되는 다양한 방법이 있었지만 (자세한 내용은 Simon Peyton Jones의 "헤어 셔츠"논문 참조) Phil Wadler가 모나드를 도입했을 때 이것이 바로 The Answer라는 것이 명백해졌습니다. 그리고 나머지는 역사입니다.


3
좀 빠지는. 모나드는 매우 오랫동안 해석을 모델링 할 수있는 것으로 알려져있다. 언어는 주변에 와서, 다음 모지 함께이 두 넣어.
nomen

1
모나드는 맵 랩과 랩핑의 동의어로 리턴으로 랩핑 해제로 정의 된 경우 이해하기 쉬울 수 있습니다.
aoeu256

9

하스켈의 불순한 계산이 모나드로 모델링 된 이유에 대해 누군가가 조언을 해 줄 수 있습니까?

음, 하스켈 때문에 순수 . 유형 수준 에서 순수하지 않은 계산순수한 계산 을 구분 하고 각각 프로그램 흐름 을 모델링 하려면 수학적 개념이 필요합니다 .

IO a, 부정확 한 계산을 모델링하는 일부 유형 을 사용해야합니다. 그런 다음의 방법을 알 필요가 결합 하는 이러한 계산 순서에 따라 적용을 ( >>=)하고 값을 올려 ( return) 가장 분명하고 기본적인 것들이다.

이 두 가지로 이미 모나드를 정의했습니다 (생각조차하지 않음).)

또한 모나드는 매우 일반적이고 강력한 추상화 를 제공하므로 많은 종류의 제어 흐름을 sequence, liftM특수 구문 과 같은 모나 딕 함수로 편리하게 일반화 할 수 있어 특별한 경우가 아닌 불쾌감을 줄 수 있습니다.

자세한 내용 은 함수형 프로그래밍고유성 입력 (내가 아는 유일한 대안)의 모나드 를 참조하십시오.


6

아시다시피 Monad, 매우 간단한 구조입니다. 답의 절반은 다음과 같습니다. Monad부작용을 일으켜 함수를 사용할 수있는 가장 간단한 구조입니다. 함께 Monad우리는이 일을 할 수있다 : 우리는 사이드 초래 값 (같은 순수 값을 처리 할 수있다 return), 우리는 새로운 측면 초래 값을 얻을 수있는 측면 초래 값 사이드 초래 기능을 적용 할 수 있습니다 ( >>=). 이 두 가지 중 하나를 수행 할 수있는 능력을 잃어 버리면 피해를 입게 될 것입니다. 따라서 부작용 유형은 "적어도"이어야 하며 지금까지 필요한 모든 것을 구현 Monad하기 Monad에 충분합니다.

나머지 절반은 : "가능한 부작용"에 우리가 줄 수있는 가장 상세한 구조는 무엇입니까? 우리는 모든 가능한 부작용의 공간을 집합으로 생각할 수 있습니다 (필요한 유일한 작업은 회원 자격입니다). 우리는 두 가지 부작용을 하나씩 차례로 수행하여 다른 부작용을 일으킬 수 있으며, 이것은 다른 부작용을 초래할 것입니다 (또는 첫 번째가 "종료 컴퓨터"이고 두 번째가 "파일 쓰기"인 경우 결과는 동일 할 것입니다) 이 구성은 "종료 컴퓨터"입니다.

자,이 작업에 대해 무엇을 말할 수 있습니까? 연관성이 있습니다. 즉, 우리가 세 가지 부작용을 결합하면 결합 순서는 중요하지 않습니다. 파일을 작성한 다음 소켓을 읽은 다음 컴퓨터를 종료하면 파일을 쓴 다음 소켓을 읽은 다음 종료하는 것과 같습니다 컴퓨터). 그러나 ( "파일 쓰기"와 "파일 삭제")는 ( "파일 삭제"와 "파일 쓰기")와는 다른 부작용입니다. 그리고 우리는 정체성을 가지고 있습니다 : "부작용 없음"효과가있는 특수 부작용 ( "부작용 없음", "파일 삭제"는 "파일 삭제"와 같은 부작용입니다))이 시점에서 모든 수학자는 "그룹!" 그러나 그룹에는 역수가 있으며 일반적으로 부작용을 반전시킬 수있는 방법은 없습니다. "파일 삭제" 돌이킬 수 없습니다. 우리가 남긴 구조는 monoid의 구조입니다. 이것은 우리의 부작용이 모나드 여야한다는 것을 의미합니다.

더 복잡한 구조가 있습니까? 확실한! 가능한 부작용을 파일 시스템 기반 효과, 네트워크 기반 효과 등으로 나눌 수 있으며 이러한 세부 사항을 보존하는보다 정교한 구성 규칙을 만들 수 있습니다. 그러나 다시 말하지만, 그것은 Monad매우 간단하지만 우리가 관심있는 대부분의 속성을 표현할만큼 강력합니다. (특히 연관성 및 기타 공리로 인해 응용 프로그램의 부작용이 조각의 부작용 조합과 동일하다는 확신을 가지고 작은 조각으로 응용 프로그램을 테스트 할 수 있습니다).


4

실제로 기능적인 방식으로 I / O를 생각하는 것은 매우 깨끗한 방법입니다.

대부분의 프로그래밍 언어에서는 입력 / 출력 작업을 수행합니다. Haskell 에서 작업 을 수행 하는 것이 아니라 원하는 작업 목록을 생성하는 코드 작성을 상상해보십시오 .

Monads는 정확히 그에 대한 구문입니다.

왜 모나드가 다른 것과 반대되는지 알고 싶다면, 사람들이 하스켈을 만들 때 생각할 수있는 I / O를 나타내는 가장 기능적인 방법이라는 것이 답입니다.


3

AFAIK, 이유는 유형 시스템에 부작용 검사를 포함 할 수 있기 때문입니다. 자세한 내용을 보려면 SE-Radio 에피소드를 시청하십시오 : 에피소드 108 : 기능 프로그래밍에 관한 Simon Peyton Jones 및 에피소드 72 : LINQ의 Erik Meijer


2

위의 이론적 배경을 가진 매우 자세한 답변이 있습니다. 그러나 IO 모나드에 대한 견해를 밝히고 싶습니다. 나는 haskell 프로그래머를 경험하지 않았으므로 아주 순진하거나 잘못되었을 수 있습니다. 그러나 IO 모나드를 어느 정도 다루는 데 도움이되었습니다 (다른 모나드와 관련이 없음).

먼저, "실제 세계"에 대한 예는 우리가 이전의 상태에 접근 할 수 없기 때문에 나에게 분명하지 않습니다. 모나드 계산과 전혀 관련이 없지만 참조 투명성의 의미에서 바람직하며 일반적으로 하스켈 코드로 표시됩니다.

그래서 우리는 우리 언어 (하스켈)가 순수하기를 원합니다. 그러나 입력 / 출력 작업이 없으면 프로그램이 유용하지 않으므로 입력 / 출력 작업이 필요합니다. 그리고 그러한 작업은 본질적으로 순수 할 수 없습니다. 따라서이를 처리하는 유일한 방법은 불순한 작업을 나머지 코드와 분리해야합니다.

여기 모나드가 온다. 실제로, 비슷한 필요한 속성을 가진 다른 구문이 존재할 수는 없지만 모나드는 이러한 속성을 가지고 있으므로 사용할 수 있으며 성공적으로 사용될 수 있습니다. 주요 재산은 우리가 탈출 할 수 없다는 것입니다. Monad 인터페이스는 우리의 가치에 대한 모나드를 제거하는 작업이 없습니다. IO가 아닌 다른 모나드는 이러한 작업을 제공하고 패턴 일치 (예 : 어쩌면)를 허용하지만 해당 작업은 모나드 인터페이스에 없습니다. 또 다른 필수 속성은 작업을 연결하는 기능입니다.

타입 시스템의 관점에서 우리가 필요로하는 것에 대해 생각한다면, 우리는 어떤 종류의 값을 감쌀 수있는 생성자를 가진 타입이 필요하다는 사실에 도달합니다. 생성자는 이스케이프 (예 : 패턴 일치)를 금지하므로 비공개이어야합니다. 그러나 우리는이 생성자에 가치를 두는 기능이 필요합니다 (여기서 귀환). 그리고 우리는 운영을 연결하는 방법이 필요합니다. 우리가 한동안 그것에 대해 생각한다면, 우리는 체인 작업이 >> =와 같은 유형을 가져야한다는 사실을 알게 될 것입니다. 그래서 우리는 모나드와 매우 비슷한 것을 보게됩니다. 이 구성으로 모순되는 상황을 분석하면 모나드 공리가 나올 것입니다.

개발 된 구조는 불순물과 공통점이 없습니다. 그것은 우리가 불순한 조작, 즉 탈출 방지, 체인 및 진입 방법을 처리 할 수 ​​있기를 원했던 속성 만 가지고 있습니다.

이제이 선택된 모나드 IO 내의 언어로 일부 불순 작업이 사전 정의됩니다. 이러한 작업을 결합하여 새로운 부정한 작업을 만들 수 있습니다. 그리고 이러한 모든 작업에는 IO 유형이 있어야합니다. 그러나 일부 기능 유형에 IO가 존재한다고해서이 기능이 불완전한 것은 아닙니다. 그러나 내가 이해하는 것처럼 처음에는 순수 함수와 불순 함수를 분리하는 것이 우리의 아이디어 였기 때문에 IO로 순수 함수를 유형으로 작성하는 것은 좋지 않습니다.

마지막으로, 모나드는 불순한 작업을 순수한 작업으로 바꾸지 않습니다. 효과적으로 분리 할 수 ​​있습니다. (나는 단지 내 이해라는 것을 반복한다)


1
그것들은 검사 효과를 입력하여 프로그램 검사를 돕고, 함수가 수행 할 수있는 효과를 제한하기 위해 모나드를 생성하여 자신의 DSL을 정의 할 수 있도록하여 컴파일러가 시퀀싱 오류를 검사 할 수 있도록합니다.
aoeu256

aoeu256의이 의견은 지금까지 제공된 모든 설명에서 누락 된 "이유"입니다. (예 : 모나드는 인간,하지만 컴파일러위한 것이 아닙니다)
주앙 테로
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.