Haskell의 순수한 기능 및 부작용 이해-putStrLn


10

최근에는 함수형 프로그래밍에 대한 지식을 넓히고 싶었 기 때문에 Haskell을 배우기 시작했으며 지금까지는 정말 사랑한다고 말해야합니다. 현재 사용중인 리소스는 Pluralsight의 'Haskell Fundamentals Part 1'코스입니다. 불행히도 다음 코드에 대한 강사의 특정 인용문을 이해하는 데 약간의 어려움이 있으며 여러분 이이 주제에 대해 밝힐 수 있기를 바랍니다.

동반 코드

helloWorld :: IO ()
helloWorld = putStrLn "Hello World"

main :: IO ()
main = do
    helloWorld
    helloWorld
    helloWorld

인용구

do-block에 동일한 IO 작업이 여러 번있는 경우 여러 번 실행됩니다. 따라서이 프로그램은 문자열 'Hello World'를 세 번 인쇄합니다. 이 예제 putStrLn는 부작용이있는 기능이 아니라는 것을 보여줍니다 . 변수 putStrLn를 정의하기 위해 함수를 한 번 호출합니다 helloWorld. 경우 putStrLn문자열을 인쇄하는 부작용이 있었다, 한 번만 인쇄 할 것이다 및 helloWorld주요 DO-블록에 반복 변수는 영향을주지 것입니다.

대부분의 다른 프로그래밍 언어에서 이와 같은 프로그램은 putStrLn함수가 호출 될 때 인쇄가 발생하므로 'Hello World'를 한 번만 인쇄 합니다. 이 미묘한 차이로 인해 초보자가 종종 트립되므로이 부분에 대해 조금 생각해보고이 프로그램이 왜 'Hello World'를 세 번 인쇄하는지 그리고 putStrLn함수가 인쇄 작업을 부작용으로 한 경우 한 번만 인쇄하는 이유를 이해해야 합니다.

내가 이해하지 못하는 것

나를 위해 문자열 'Hello World'가 세 번 인쇄되는 것이 거의 당연합니다. helloWorld변수 (또는 함수?)를 나중에 호출되는 일종의 콜백으로 인식합니다 . 내가 이해하지 못하는 putStrLn것은 부작용이 있다면 문자열이 한 번만 인쇄되는 방법입니다. 또는 다른 프로그래밍 언어로 한 번만 인쇄되는 이유는 무엇입니까?

C # 코드에서 다음과 같이 가정합니다.

C # (피들)

using System;

public class Program
{
    public static void HelloWorld()
    {
        Console.WriteLine("Hello World");
    }

    public static void Main()
    {
        HelloWorld();
        HelloWorld();
        HelloWorld();
    }
}

나는 그의 용어를 아주 단순하게 간과하거나 잘못 해석하고 있다고 확신한다. 도움을 주시면 감사하겠습니다.

편집하다:

답변 해 주셔서 감사합니다. 귀하의 답변으로 이러한 개념을 더 잘 이해할 수있었습니다. 아직 완전히 클릭 한 것 같지는 않지만 앞으로이 주제를 다시 방문하겠습니다. 감사합니다!


2
helloWorldC #에서 필드 또는 변수와 같이 일정하다고 생각하십시오 . 에 적용되는 매개 변수가 없습니다 helloWorld.
Caramiriel

2
putStrLn 부작용 이 없습니다 . 호출 횟수에 관계없이 인수에 대해 동일한 IO 조치, 단순히 IO 조치를 리턴합니다 . "Hello World"putStrLn
chepner

1
만약 그렇다면, helloworld인쇄하는 행동이 아닐 것입니다 Hello world; 이것은 반환하는 값이 될 것이다 putStrLn 후에 프린트 Hello World(즉, ()).
chepner

2
이 예를 이해하면 Haskell에서 부작용이 어떻게 작동하는지 이미 이해해야합니다. 좋은 예가 아닙니다.
user253751

C # 스 니펫에서는을 좋아하지 않습니다 helloWorld = Console.WriteLine("Hello World");. 당신은 단지를 포함 Console.WriteLine("Hello World");HelloWorld기능을 실행하기 위해 매번 HelloWorld호출됩니다. 이제 무엇을 helloWorld = putStrLn "Hello World"만드는지 생각해보십시오 helloWorld. 이 포함 된 IO 모나드에 할당됩니다 (). 바인딩하면 바인딩 >>=작업 만 수행하고 (인쇄) (), 바인드 연산자의 오른쪽에 표시됩니다.
Redu

답변:


8

helloWorld로컬 변수로 정의하면 저자가 의미하는 바를 이해하는 것이 더 쉬울 것입니다 .

main :: IO ()
main = do
  let helloWorld = putStrLn "Hello World!"
  helloWorld
  helloWorld
  helloWorld

이 C # 유사 의사 코드와 비교할 수 있습니다.

void Main() {
  var helloWorld = {
    WriteLine("Hello World!")
  }
  helloWorld;
  helloWorld;
  helloWorld;
}

즉 C # WriteLine의 인수는 인수를 인쇄하고 아무것도 반환하지 않는 절차입니다. Haskell에서는 putStrLn문자열을 가져 와서 해당 문자열을 인쇄하는 작업을 수행하는 함수입니다. 그것은 글쓰기 사이에 전혀 차이가 없다는 것을 의미합니다

do
  let hello = putStrLn "Hello World"
  hello
  hello

do
  putStrLn "Hello World"
  putStrLn "Hello World"

즉,이 예제에서 그 차이는 특별히 심오하지 않으므로 저자 가이 섹션에서 얻는 것을 얻지 못하고 지금 당장 나아가는 것이 좋습니다.

파이썬과 비교하면 조금 더 잘 작동합니다.

hello_world = print('hello world')
hello_world
hello_world
hello_world

요점은 여기 하스켈의 IO 작업이 실행에서 그들을 방지하기 위해 더 "콜백"또는 종류의 아무것도에 싸여 할 필요가없는 "진짜"값이 있다는 것 -이 아니라, 유일한 방법은하기 IS를 실행하기 위해 그들을 얻을 그것들을 특정 장소 (즉, 내부 main나 실이 튀어 나온 곳)에 넣습니다 main.

이것은 단지 팔러 트릭 일뿐 만 아니라 코드 작성 방법에 흥미로운 영향을 미칩니다 (예 : Haskell이 익숙한 공통 제어 구조가 실제로 필요하지 않은 이유의 일부입니다) 명령형 언어를 사용하고 대신 기능 측면에서 모든 작업을 수행하지 않아도 됨) 다시 걱정하지 않아도됩니다 (이러한 분석이 항상 즉시 클릭되는 것은 아닙니다)


4

대신 실제로 무언가를 수행하는 함수를 사용하는 경우 설명 된대로 차이를 확인하는 것이 더 쉬울 수 있습니다 helloWorld. 다음을 생각하십시오.

add :: Int -> Int -> IO Int
add x y = do
  putStrLn ("I am adding " ++ show x ++ " and " ++ show y)
  return (x + y)

plus23 :: IO Int
plus23 = add 2 3

main :: IO ()
main = do
  _ <- plus23
  _ <- plus23
  _ <- plus23
  return ()

"2와 3을 추가하고 있습니다"가 3 번 인쇄됩니다.

C #에서는 다음을 작성할 수 있습니다.

using System;

public class Program
{
    public static int add(int x, int y)
    {
        Console.WriteLine("I am adding {0} and {1}", x, y);
        return x + y;
    }

    public static void Main()
    {
        int x;
        int plus23 = add(2, 3);
        x = plus23;
        x = plus23;
        x = plus23;
        return;
    }
}

한 번만 인쇄됩니다.


3

평가 putStrLn "Hello World"결과 부작용이 있으면 메시지가 한 번만 인쇄됩니다.

다음 코드를 사용하여 해당 시나리오를 근사화 할 수 있습니다.

import System.IO.Unsafe (unsafePerformIO)
import Control.Exception (evaluate)

helloWorld :: ()
helloWorld = unsafePerformIO $ putStrLn "Hello World"

main :: IO ()
main = do
    evaluate helloWorld
    evaluate helloWorld
    evaluate helloWorld

unsafePerformIOIO행동을 취하고 그것을 잊어 버린다. 그것은 IO행동의 구성에 의해 부과 된 일반적인 시퀀싱 IO에서 풀고, 게으른 평가의 결과에 따라 효과가 일어나도록 (또는하지 않도록) 행동이다.

evaluate순수한 가치를 취하고 결과적인 IO행동 이 평가 될 때마다 가치가 평가되도록 보장합니다 main. 여기에서는이 값을 사용하여 일부 값의 평가를 프로그램의 실행에 연결합니다.

이 코드는 "Hello World"를 한 번만 인쇄합니다. 우리 helloWorld는 순수한 가치로 취급 합니다. 그러나 이는 모든 evaluate helloWorld통화 간에 공유됩니다 . 왜 안돼? 결국 순수한 가치입니다. 왜 불필요하게 다시 계산합니까? 첫 번째 evaluate작업은 "숨겨진"효과를 "팝업"하고 나중 작업은 결과를 평가하므로 ()더 이상의 효과는 발생하지 않습니다.


1
unsafePerformIOHaskell을 배우는이 단계에서 절대 사용해서는 안된다는 점에 주목할 가치가 있습니다 . 이유에 따라 이름에 "안전하지 않은"것이 있으며, 문맥에서 사용의 의미를 신중하게 고려할 수없는 경우 (또는 사용하지 않은 경우) 사용해서는 안됩니다. danidiaz가 답변에 넣은 코드는에서 발생할 수있는 직관적이지 않은 행동을 완벽하게 포착합니다 unsafePerformIO.
앤드류 레이

1

주의해야 할 한 가지 세부 사항이 있습니다 . putStrLn정의하는 동안 함수를 한 번만 호출 합니다 helloWorld. 에서 main기능 당신은 단지 그것의 반환 값을 사용하여 putStrLn "Hello, World"세 번.

강사는 putStrLn전화에 부작용이 없으며 사실 이라고 말합니다 . 그러나 유형을 살펴보십시오 helloWorld. IO 동작입니다. putStrLn그냥 당신을 위해 그것을 만듭니다. 나중에 3 개를 do블록과 연결하여 다른 IO 동작을 만듭니다 main. 나중에 프로그램을 실행하면 해당 작업이 실행되며 부작용이 발생합니다.

모나드의 기반이되는 메커니즘 . 이 강력한 개념을 사용하면 부작용을 직접 지원하지 않는 언어로 인쇄하는 것과 같은 부작용을 사용할 수 있습니다. 몇 가지 동작을 연결하면 해당 시작은 프로그램 시작시 실행됩니다. Haskell을 진지하게 사용하려면이 개념을 깊이 이해해야합니다.

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