Development Tip

Haskell 상태 모나드를 사용하면 코드 냄새가나요?

yourdevel 2020. 12. 25. 10:35
반응형

Haskell 상태 모나드를 사용하면 코드 냄새가나요?


세상에 나는 "코드 냄새"라는 용어를 싫어하지만 더 정확한 것은 생각할 수 없다.

저는 여가 시간에 컴파일러 구성, 언어 디자인 및 함수 프로그래밍에 대해 배우기 위해 공백대한 고급 언어 및 컴파일러를 설계하고 있습니다 (컴파일러는 Haskell로 작성 중입니다).

컴파일러의 코드 생성 단계에서 구문 트리를 탐색 할 때 "상태"데이터를 유지해야합니다. 예를 들어 흐름 제어 문을 컴파일 할 때 이동할 레이블의 고유 한 이름을 생성해야합니다 (전달, 업데이트 및 반환 된 카운터에서 생성 된 레이블 및 카운터의 이전 값은 다시 사용해서는 안 됨). 또 다른 예는 구문 트리에서 인라인 문자열 리터럴을 발견하면 영구적으로 힙 변수로 변환해야한다는 것입니다 (공백에서 문자열은 힙에 가장 잘 저장 됨). 나는 현재 이것을 처리하기 위해 상태 모나드에서 전체 코드 생성 모듈을 래핑하고 있습니다.

컴파일러를 작성하는 것이 기능적 패러다임에 잘 맞는 문제라는 말을 들었지만, C로 디자인하는 것과 거의 같은 방식으로 이것을 디자인하고 있다는 것을 알게되었습니다 (실제로 C를 어떤 언어로도 작성할 수 있습니다. 상태 모나드가있는 Haskell).

Haskell 구문을 사용하는 C가 아닌 Haskell (기능적 패러다임이 아니라)에서 생각하는 방법을 배우고 싶습니다. 상태 모나드의 사용을 제거 / 최소화해야합니까? 아니면 합법적 인 기능 "디자인 패턴"입니까?


일반적으로 상태는 작고 잘 제어되는 한 코드 냄새가 아니라고 말하고 싶습니다.

즉, State, ST 또는 사용자 정의 빌드와 같은 모나드를 사용하거나 몇 군데로 전달하는 상태 데이터를 포함하는 데이터 구조를 갖는 것은 나쁜 일이 아닙니다. (사실, 모나드는 정확히이 작업을 수행하는 데 도움이됩니다!) 그러나 모든 곳에서 상태 (예, 이것은 당신을 의미합니다, IO 모나드!)는 나쁜 냄새입니다.

이에 대한 상당히 명확한 예는 우리 팀이 ICFP 프로그래밍 경연 대회 2009 (코드는 git : //git.cynic.net/haskell/icfp-contest-2009에서 사용할 수 있음)에 출품 할 때 작업 할 때였습니다 . 우리는 이에 대해 몇 가지 다른 모듈 식 부품으로 끝났습니다.

  • VM : 시뮬레이션 프로그램을 실행 한 가상 머신
  • 컨트롤러 : 시뮬레이터의 출력을 읽고 새로운 제어 입력을 생성하는 여러 가지 루틴 세트
  • 솔루션 : 컨트롤러의 출력을 기반으로 솔루션 파일 생성
  • 시각화 도우미 : 입력 및 출력 포트를 모두 읽고 시뮬레이션이 진행됨에 따라 진행되는 작업에 대한 일종의 시각화 또는 로그를 생성하는 여러 가지 루틴 세트

이들 각각은 자체 상태를 가지며 VM의 입력 및 출력 값을 통해 다양한 방식으로 상호 작용합니다. 우리는 각각 다른 종류의 상태를 가지고있는 여러 컨트롤러와 시각화 장치를 가지고있었습니다.

여기서 요점은 특정 상태의 내부가 자신의 특정 모듈로 제한되었고 각 모듈은 다른 모듈의 상태 존재조차 알지 못했다는 것입니다. 특정 상태 저장 코드 및 데이터 세트는 일반적으로 수십 줄에 불과했으며 상태에 소수의 데이터 항목이 있습니다.

이 모든 것은 상태의 내부에 접근 할 수 없었고 시뮬레이션을 반복하면서 적절한 순서로 올바른 것을 호출하고 매우 제한된 (물론 모듈의 이전 상태와 함께) 각 모듈에 대한 외부 정보의 양.

상태가 이렇게 제한된 방식으로 사용되고 유형 시스템이 실수로 상태를 수정하는 것을 방지하는 경우 처리하기가 매우 쉽습니다. 이것은 Haskell의 아름다움 중 하나입니다.

한 가지 대답은 "모나드를 사용하지 마십시오."라고 말합니다. 내 관점에서 이것은 정확히 역순입니다. 모나드는 무엇보다도 상태에 닿는 코드의 양을 최소화하는 데 도움이 될 수있는 제어 구조입니다. 예를 들어 모나 딕 파서를 살펴보면 파싱 상태 (즉, 파싱되는 텍스트, 텍스트가 얼마나 멀리 도달했는지, 누적 된 경고 등)는 파서에서 사용되는 모든 결합자를 통해 실행되어야합니다. . 그러나 실제로 상태를 직접 조작하는 조합자는 몇 개뿐입니다. 다른 것은이 몇 가지 기능 중 하나를 사용합니다. 이를 통해 상태를 변경할 수있는 소량의 코드를 모두 한곳에서 명확하게 볼 수 있으며 변경 방법에 대해 더 쉽게 추론하여 처리하기가 더 쉽습니다.


Haskell에서 여러 컴파일러를 작성했으며 상태 모나드는 많은 컴파일러 문제에 대한 합리적인 해결책입니다. 그러나 당신은 그것을 추상적으로 유지하기를 원합니다 .-- 당신이 모나드를 사용하고 있다는 것을 명백하게하지 마십시오.

다음은 Glasgow Haskell 컴파일러의 예입니다 (필자가 작성 하지 않았습니다 . 몇 가지 가장자리 만 처리했습니다). 여기서 제어 흐름 그래프를 작성합니다. 그래프를 만드는 기본 방법은 다음과 같습니다.

empyGraph    :: Graph
mkLabel      :: Label -> Graph
mkAssignment :: Assignment -> Graph  -- modify a register or memory
mkTransfer   :: ControlTransfer -> Graph   -- any control transfer
(<*>)        :: Graph -> Graph -> Graph

하지만 아시다시피 고유 한 라벨을 유지하는 것은 지루한 일이므로 다음과 같은 기능도 제공합니다.

withFreshLabel :: (Label -> Graph) -> Graph
mkIfThenElse :: (Label -> Label -> Graph) -- branch condition
             -> Graph   -- code in the 'then' branch
             -> Graph   -- code in the 'else' branch 
             -> Graph   -- resulting if-then-else construct

모든 Graph것은 추상적 인 유형이며 번역자는 모나 딕이 진행되고 있다는 것을 알지 못하고 순전히 기능적인 방식으로 그래프를 즐겁게 구성합니다. 그런 다음 그래프가 최종적으로 생성되면 코드를 생성 할 수있는 대수 데이터 유형으로 변환하기 위해 고유 한 레이블을 제공하고 상태 모나드를 실행하고 데이터 구조를 가져옵니다.

상태 모나드는 그 아래에 숨겨져 있습니다. 클라이언트에 노출되지는 않지만 정의 Graph는 다음과 같습니다.

type Graph = RealGraph -> [Label] -> (RealGraph, [Label])

또는 조금 더 정확하게

type Graph = RealGraph -> State [Label] RealGraph
  -- a Graph is a monadic function from a successor RealGraph to a new RealGraph

추상화 레이어 뒤에 숨겨진 상태 모나드는 전혀 냄새가 나지 않습니다!


특성 문법 (AG) 을 살펴 보셨습니까 ? ( 위키 백과에 대한 추가 정보 Monad Reader 기사 )?

AG를 사용하면 구문 트리에 속성추가 할 수 있습니다 . 이러한 속성은 합성 속성 상속 속성으로 구분됩니다 .

합성 된 속성은 구문 트리에서 생성 (또는 합성)하는 항목으로, 생성 된 코드 또는 모든 주석 또는 기타 관심있는 항목이 될 수 있습니다.

상속 된 속성은 구문 트리에 입력되며, 환경 또는 코드 생성 중에 사용할 레이블 목록 일 수 있습니다.

Utrecht University에서는 컴파일러를 작성 하기 위해 속성 문법 시스템 ( UUAGC )을 사용합니다 . .hs제공된 .ag파일 에서 하스켈 코드 ( 파일) 를 생성하는 전 처리기입니다 .


Although, if you're still learning Haskell, then maybe this is not the time to start learning yet another layer of abstraction over that.

In that case, you could manually write the sort of code that attributes grammars generate for you, for example:

data AbstractSyntax = Literal Int | Block AbstractSyntax
                    | Comment String AbstractSyntax

compile :: AbstractSyntax -> [Label] -> (Code, Comments)
compile (Literal x) _      = (generateCode x, [])
compile (Block ast) (l:ls) = let (code', comments) = compile ast ls
                             in (labelCode l code', comments)
compile (Comment s ast) ls = let (code, comments') = compile ast ls
                             in (code, s : comments')

generateCode :: Int -> Code
labelCode :: Label -> Code -> Code

It's possible that you may want an applicative functor instead of a monad:

http://www.haskell.org/haskellwiki/Applicative_functor

I think the original paper explains it better than the wiki, however:

http://www.soi.city.ac.uk/~ross/papers/Applicative.html


I don't think using the State Monad is a code smell when it used to model state.

If you need to thread state through your functions, you can do this explicitly, taking the the state as an argument and returning it in each function. The State Monad offers a good abstraction: it passes the state along for you and provides lots of useful function to combine functions that require state. In this case, using the State Monad (or Applicatives) is not a code smell.

However, if you use the State Monad to emulate an imperative style of programming while a functional solution would suffice, you are just making things complicated.


In general you should try to avoid state wherever possible, but that's not always practical. Applicative makes effectful code look nicer and more functional, especially tree traversal code can benefit from this style. For the problem of name generation there is now a rather nice package available: value-supply.


Well, don't use monads. The power of functional programming is function purity and their reuse. There's this paper a professor of mine once wrote and he's one of the guys who helped build Haskell.

The paper is called "Why functional programming matters", I suggest you read through it. It's a good read.


let's be careful about the terminology here. State is not per se bad; functional languages have state. What is a "code smell" is when you find yourself wanting to assign variables values and change them.

Of course, the Haskell state monad is there for just that reason -- as with I/O, it's letting you do unsafe and un-functional things in a constrained context.

So, yes, it's probably a code smell.

ReferenceURL : https://stackoverflow.com/questions/607830/use-of-haskell-state-monad-a-code-smell

반응형