Hiểu các chức năng thuần túy và tác dụng phụ trong Haskell - putStrLn


10

Gần đây, tôi bắt đầu học Haskell vì tôi muốn mở rộng kiến ​​thức về lập trình chức năng và tôi phải nói rằng tôi thực sự yêu thích nó cho đến nay. Tài nguyên tôi hiện đang sử dụng là khóa học 'Haskell Fund basicals Part 1' về Pluralsight. Thật không may, tôi có một số khó khăn để hiểu một trích dẫn cụ thể của giảng viên về mã sau đây và hy vọng các bạn có thể làm sáng tỏ chủ đề này.

Mã kèm theo

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

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

Trích dẫn

Nếu bạn có cùng một hành động IO nhiều lần trong một khối do, nó sẽ được chạy nhiều lần. Vì vậy, chương trình này in ra chuỗi 'Hello World' ba lần. Ví dụ này giúp minh họa rằng đó putStrLnkhông phải là một chức năng với các tác dụng phụ. Chúng ta gọi putStrLnhàm một lần để xác định helloWorldbiến. Nếu putStrLncó tác dụng phụ là in chuỗi, nó sẽ chỉ in một lần và helloWorldbiến được lặp lại trong khối do chính sẽ không có tác dụng.

Trong hầu hết các ngôn ngữ lập trình khác, một chương trình như thế này sẽ chỉ in 'Hello World' một lần, vì việc in sẽ xảy ra khi putStrLnchức năng được gọi. Sự khác biệt tinh tế này thường làm vấp ngã những người mới bắt đầu, vì vậy hãy suy nghĩ về điều này một chút và đảm bảo bạn hiểu lý do tại sao chương trình này in 'Hello World' ba lần và tại sao nó chỉ in một lần nếu putStrLnchức năng in là hiệu ứng phụ.

Những gì tôi không hiểu

Đối với tôi, dường như gần như tự nhiên rằng chuỗi 'Hello World' được in ba lần. Tôi cảm nhận helloWorldbiến (hoặc hàm?) Như một kiểu gọi lại được gọi sau này. Điều tôi không hiểu là, làm thế nào nếu putStrLncó tác dụng phụ, nó sẽ dẫn đến chuỗi chỉ được in một lần. Hoặc tại sao nó chỉ được in một lần bằng các ngôn ngữ lập trình khác.

Giả sử trong mã C #, tôi sẽ cho rằng nó sẽ trông như thế này:

C # (Câu đố)

using System;

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

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

Tôi chắc chắn rằng tôi đang xem xét một cái gì đó khá đơn giản hoặc giải thích sai thuật ngữ của anh ấy. Mọi sự trợ giúp sẽ rất được trân trọng.

BIÊN TẬP:

Cảm ơn tất cả các câu trả lời của bạn! Câu trả lời của bạn đã giúp tôi hiểu rõ hơn về các khái niệm này. Tôi không nghĩ rằng nó đã được nhấp hoàn toàn, nhưng tôi sẽ xem lại chủ đề trong tương lai, cảm ơn bạn!


2
Hãy nghĩ về helloWorldhằng số như một trường hoặc biến trong C #. Không có tham số nào đang được áp dụng helloWorld.
Caramiriel

2
putStrLn không có tác dụng phụ; nó chỉ đơn giản trả về một hành động IO, hành động IO tương tự cho đối số cho "Hello World"dù bạn gọi bao nhiêu lần putStrLn.
chepner

1
Nếu nó đã làm, helloworldsẽ không phải là một hành động in Hello world; nó sẽ là giá trị trả về bởi putStrLn sau khi được in Hello World(cụ thể là, ()).
chepner

2
Tôi nghĩ để hiểu ví dụ này, bạn sẽ phải hiểu tác dụng phụ hoạt động như thế nào trong Haskell. Đó không phải là một ví dụ tốt.
dùng253751

Trong đoạn mã C # của bạn, bạn không thích helloWorld = Console.WriteLine("Hello World");. Bạn chỉ chứa hàm Console.WriteLine("Hello World");trong HelloWorldhàm được thực thi mỗi khi HelloWorldđược gọi. Bây giờ nghĩ về những gì helloWorld = putStrLn "Hello World"làm cho helloWorld. Nó được gán cho một đơn vị IO có chứa (). Khi bạn liên kết nó, >>=nó sẽ chỉ thực hiện hoạt động của nó (in một cái gì đó) và sẽ cung cấp cho bạn ()ở phía bên phải của toán tử liên kết.
Redu

Câu trả lời:


8

Có lẽ sẽ dễ hiểu hơn về ý nghĩa của tác giả nếu chúng ta định nghĩa helloWorldlà một biến cục bộ:

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

mà bạn có thể so sánh với mã giả giống C # này:

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

Tức là trong C # WriteLinelà một thủ tục in đối số của nó và không trả về gì cả. Trong Haskell, putStrLnlà một hàm lấy một chuỗi và cung cấp cho bạn một hành động sẽ in chuỗi đó để nó được thực thi. Nó có nghĩa là hoàn toàn không có sự khác biệt giữa văn bản

do
  let hello = putStrLn "Hello World"
  hello
  hello

do
  putStrLn "Hello World"
  putStrLn "Hello World"

Điều đó đã được nói, trong ví dụ này, sự khác biệt không đặc biệt sâu sắc, vì vậy thật tốt nếu bạn không hiểu rõ những gì tác giả đang cố gắng hiểu trong phần này và chỉ cần chuyển sang bây giờ.

nó hoạt động tốt hơn một chút nếu bạn so sánh nó với python

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

Vấn đề ở đây là các hành động IO trong Haskell là các giá trị Real thực sự không cần phải được bao bọc trong các cuộc gọi lại tiếp theo, hay bất cứ điều gì tương tự để ngăn chúng thực thi - thay vào đó, cách duy nhất để thực hiện chúng để đặt chúng ở một nơi cụ thể (nghĩa là ở đâu đó bên trong mainhoặc một sợi được sinh ra main).

Đây cũng không phải là một trò lừa bịp, điều này cuối cùng cũng có một số hiệu ứng thú vị về cách bạn viết mã (ví dụ, đó là một phần lý do tại sao Haskell không thực sự cần bất kỳ cấu trúc điều khiển phổ biến nào bạn quen thuộc với các ngôn ngữ bắt buộc và có thể thoát khỏi việc thực hiện mọi thứ về chức năng thay thế), nhưng một lần nữa tôi sẽ không lo lắng quá nhiều về điều này (tương tự như thế này không phải lúc nào cũng nhấp ngay lập tức)


4

Có thể dễ dàng hơn để thấy sự khác biệt như được mô tả nếu bạn sử dụng một chức năng thực sự làm một cái gì đó, hơn là helloWorld. Hãy nghĩ về những điều sau đây:

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 ()

Điều này sẽ in ra "Tôi đang thêm 2 và 3" 3 lần.

Trong C #, bạn có thể viết như sau:

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;
    }
}

Mà sẽ chỉ in một lần.


3

Nếu đánh giá putStrLn "Hello World"có tác dụng phụ, thì thông điệp sẽ chỉ được in một lần.

Chúng ta có thể tính gần đúng kịch bản đó với đoạn mã sau:

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

unsafePerformIOthực hiện một IOhành động và "quên" đó là một IOhành động, hủy bỏ nó khỏi trình tự thông thường được áp đặt bởi các thành phần của IOhành động và để cho hiệu ứng diễn ra (hoặc không) theo sự mơ hồ của đánh giá lười biếng.

evaluatelấy một giá trị thuần túy và đảm bảo rằng giá trị đó được đánh giá bất cứ khi nào IOhành động kết quả được đánh giá là điều mà đối với chúng tôi sẽ là vì nó nằm trong đường dẫn của main. Chúng tôi đang sử dụng nó ở đây để kết nối việc đánh giá một số giá trị với sự xuất hiện của chương trình.

Mã này chỉ in "Hello World" một lần. Chúng tôi coi helloWorldnhư một giá trị thuần túy. Nhưng điều đó có nghĩa là nó sẽ được chia sẻ giữa tất cả các evaluate helloWorldcuộc gọi. Và tại sao không? Rốt cuộc nó là một giá trị thuần túy, tại sao lại tính toán lại một cách không cần thiết? evaluateHành động đầu tiên "bật" hiệu ứng "ẩn" và các hành động sau chỉ đánh giá kết quả (), điều này không gây ra bất kỳ hiệu ứng nào nữa.


1
Điều đáng chú ý là bạn tuyệt đối không nên sử dụng unsafePerformIOở giai đoạn học Haskell này. Nó có "không an toàn" trong tên vì một lý do và bạn không nên sử dụng nó trừ khi bạn có thể (và đã) xem xét cẩn thận ý nghĩa của việc sử dụng nó trong ngữ cảnh. Mã mà danidiaz đưa vào câu trả lời hoàn toàn nắm bắt được loại hành vi không trực quan có thể xảy ra unsafePerformIO.
Andrew Ray

1

Có một chi tiết cần chú ý: bạn chỉ gọi putStrLnchức năng một lần, trong khi xác định helloWorld. Trong mainhàm bạn chỉ cần sử dụng giá trị trả về của putStrLn "Hello, World"ba lần đó.

Giảng viên nói rằng putStrLncuộc gọi không có tác dụng phụ và đó là sự thật. Nhưng hãy nhìn vào loại helloWorld- đó là một hành động IO. putStrLnchỉ tạo ra nó cho bạn Sau đó, bạn xâu chuỗi 3 trong số chúng với dokhối để tạo một hành động IO khác - main. Sau đó, khi bạn thực hiện chương trình của mình, hành động đó sẽ được chạy, đó là nơi tác dụng phụ nằm.

Cơ chế đó nằm trong cơ sở này - monads . Khái niệm mạnh mẽ này cho phép bạn sử dụng một số tác dụng phụ như in bằng ngôn ngữ không hỗ trợ trực tiếp các tác dụng phụ. Bạn chỉ cần xâu chuỗi một số hành động và chuỗi đó sẽ được chạy khi bắt đầu chương trình của bạn. Bạn sẽ cần hiểu khái niệm đó sâu sắc nếu bạn muốn sử dụng Haskell một cách nghiêm túc.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.