[haskell] Lazy I / O의 나쁜 점은 무엇입니까?

일반적으로 프로덕션 코드는 Lazy I / O를 사용하지 않아야한다고 들었습니다. 내 질문은, 왜? 장난을 치지 않고 Lazy I / O를 사용해도 괜찮습니까? 그리고 대안 (예 : 열거 자)을 더 좋게 만드는 것은 무엇입니까?



답변

Lazy IO는 프로그램이 데이터를 소비하는 방식에 따라 “수요 패턴”과 같이 획득 한 리소스를 해제하는 것이 다소 예측할 수 없다는 문제가 있습니다. 프로그램이 리소스에 대한 마지막 참조를 삭제하면 GC는 결국 해당 리소스를 실행하고 해제합니다.

Lazy 스트림은 프로그래밍하기에 매우 편리한 스타일입니다. 이것이 쉘 파이프가 매우 재미 있고 인기있는 이유입니다.

그러나 리소스가 제한된 경우 (고성능 시나리오 또는 머신의 한계까지 확장 될 것으로 예상되는 프로덕션 환경에서) 정리를 위해 GC에 의존하는 것은 충분하지 않을 수 있습니다.

확장 성을 높이기 위해 리소스를 열심히 해제해야하는 경우가 있습니다.

그렇다면 점진적 처리를 포기하는 것을 의미하지 않는 지연 IO의 대안은 무엇일까요 (즉, 너무 많은 리소스를 소비하게 됨)? 글쎄요, 우리는 2000 년대 후반 Oleg Kiselyov에foldl 의해 도입 된 이후 많은 네트워킹 기반 프로젝트에 의해 대중화 된 기반 처리, 일명 반복 또는 열거자를 가지고 있습니다.

데이터를 지연 스트림으로 처리하거나 하나의 거대한 배치로 처리하는 대신, 대신 청크 기반의 엄격한 처리를 추상화하여 마지막 청크를 읽은 후 리소스의 확정을 보장합니다. 이것이 iteratee 기반 프로그래밍의 본질이며 매우 좋은 리소스 제약을 제공하는 것입니다.

iteratee 기반 IO의 단점은 다소 어색한 프로그래밍 모델이 있다는 것입니다 (멋진 스레드 기반 제어에 비해 이벤트 기반 프로그래밍과 거의 유사 함). 모든 프로그래밍 언어에서 확실히 고급 기술입니다. 그리고 대부분의 프로그래밍 문제에서 지연 IO는 전적으로 만족 스럽습니다. 그러나 많은 파일을 열거 나 많은 소켓에서 통신하거나 동시에 많은 리소스를 사용하는 경우 iteratee (또는 열거 자) 접근 방식이 적합 할 수 있습니다.


답변

Dons는 매우 좋은 대답을 제공했지만 반복의 가장 매력적인 기능 중 하나 인 (나에게) 무엇을 빠뜨 렸습니다. 오래된 데이터를 명시 적으로 유지해야하기 때문에 공간 관리에 대해 더 쉽게 추론 할 수 있습니다. 중히 여기다:

average :: [Float] -> Float
average xs = sum xs / length xs

및을 xs모두 계산하려면 전체 목록 을 메모리에 유지해야 하므로 잘 알려진 공간 누수 입니다. 접기를 만들어 효율적인 소비자를 만드는 것이 가능합니다.sumlength

average2 :: [Float] -> Float
average2 xs = uncurry (/) <$> foldl (\(sumT, n) x -> (sumT+x, n+1)) (0,0) xs
-- N.B. this will build up thunks as written, use a strict pair and foldl'

그러나 모든 스트림 프로세서에 대해이 작업을 수행해야하는 것은 다소 불편합니다. 몇 가지 일반화 ( Conal Elliott-Beautiful Fold Zipping )가 있지만 그다지 포착되지 않은 것 같습니다. 그러나 반복은 비슷한 수준의 표현을 얻을 수 있습니다.

aveIter = uncurry (/) <$> I.zip I.sum I.length

목록이 여러 번 반복되기 때문에 접기만큼 효율적이지 않지만 청크로 수집되므로 오래된 데이터를 효율적으로 가비지 수집 할 수 있습니다. 해당 속성을 중단하려면 stream2list와 같이 전체 입력을 명시 적으로 유지해야합니다.

badAveIter = (\xs -> sum xs / length xs) <$> I.stream2list

프로그래밍 모델로서의 반복 상태는 진행중인 작업이지만 1 년 전보다 훨씬 좋습니다. 유용한 무엇 콤비 우린 학습 (예를 들면 zip, breakE, enumWith) 어느 iteratees에 내장 콤비가 지속적으로 더 표현력을 제공, 그 결과 덜 수 있습니다.

즉, Dons는 그들이 고급 기술이라는 것이 맞습니다. 확실히 모든 I / O 문제에 사용하지는 않을 것입니다.


답변

저는 프로덕션 코드에서 항상 lazy I / O를 사용합니다. Don이 언급 한 것처럼 특정 상황에서만 문제입니다. 그러나 몇 개의 파일을 읽는 경우에는 잘 작동합니다.


답변

업데이트 : 최근 하스켈 – 카페에 올렉 Kiseljov 보여 주었다unsafeInterleaveST(ST는 모나드 내 게으른 IO를 구현하기 위해 사용된다) 매우 안전하지 않은 – 그것은 등식 추론을 나누기. 그는 그것이 다음 bad_ctx :: ((Bool,Bool) -> Bool) -> Bool
과 같이 구성 할 수 있음을 보여줍니다.

> bad_ctx (\(x,y) -> x == y)
True
> bad_ctx (\(x,y) -> y == x)
False

비록 ==교환 적이 지만 .


지연 IO의 또 다른 문제 : 실제 IO 작업은 너무 늦을 때까지 지연 될 수 있습니다 (예 : 파일이 닫힌 후). Haskell Wiki 에서 인용 -게으른 IO 문제 :

예를 들어, 일반적인 초보자 실수는 파일 읽기를 마치기 전에 파일을 닫는 것입니다.

wrong = do
    fileData <- withFile "test.txt" ReadMode hGetContents
    putStr fileData

문제는 withFile이 fileData가 강제 실행되기 전에 핸들을 닫는다는 것입니다. 올바른 방법은 모든 코드를 withFile에 전달하는 것입니다.

right = withFile "test.txt" ReadMode $ \handle -> do
    fileData <- hGetContents handle
    putStr fileData

여기서 데이터는 withFile이 완료되기 전에 사용됩니다.

이것은 종종 예상치 못한 오류이며 만들기 쉬운 오류입니다.


참조 : Lazy I / O 문제의 세 가지 예 .


답변

지금까지 언급되지 않은 지연 IO의 또 다른 문제는 놀라운 동작이 있다는 것입니다. 일반적인 Haskell 프로그램에서는 프로그램의 각 부분이 언제 평가되는지 예측하기가 어려울 수 있지만 다행스럽게도 순도 때문에 성능 문제가없는 한 실제로 중요하지 않습니다. 지연 IO가 도입되면 코드의 평가 순서가 실제로 의미에 영향을 미치므로 무해하다고 생각하는 데 익숙한 변경 사항은 진정한 문제를 일으킬 수 있습니다.

예를 들어, 합리적으로 보이지만 지연된 IO로 인해 더 혼란스러운 코드에 대한 질문이 있습니다. withFile 대 openFile

이러한 문제는 항상 치명적이지는 않지만 생각해야 할 또 다른 문제이며 모든 작업을 미리 수행하는 데 실제 문제가없는 한 개인적으로 게으른 IO를 피할 수있는 충분히 심한 두통입니다.


답변