Là một học viên, tại sao tôi phải quan tâm đến Haskell? Một đơn nguyên là gì và tại sao tôi cần nó? [đóng cửa]


9

Tôi không hiểu vấn đề họ giải quyết.


1
Gần như là một bản sao của: lập trình
viên.stackexchange.com/questions/25569 / trộm

2
Tôi nghĩ rằng chỉnh sửa này là một chút cực đoan. Tôi nghĩ rằng câu hỏi của bạn về cơ bản là một câu hỏi hay. Chỉ là một số phần của nó là một chút ... tranh luận. Đó có lẽ chỉ là kết quả của sự thất vọng khi cố gắng học một cái gì đó mà bạn không thấy được.
Jason Baker

@SnOrfus, tôi là người khốn nạn câu hỏi. Tôi đã quá lười biếng để chỉnh sửa nó đúng cách.
Công việc

Câu trả lời:


34

Monads không tốt cũng không xấu. Họ chỉ là. Chúng là những công cụ được sử dụng để giải quyết các vấn đề như nhiều cấu trúc ngôn ngữ lập trình khác. Một ứng dụng rất quan trọng của chúng là làm cho cuộc sống dễ dàng hơn cho các lập trình viên làm việc trong một ngôn ngữ hoàn toàn chức năng. Nhưng chúng hữu ích trong các ngôn ngữ phi chức năng; chỉ là mọi người hiếm khi nhận ra họ đang sử dụng Monad.

Một đơn nguyên là gì? Cách tốt nhất để nghĩ về Monad là một mẫu thiết kế. Trong trường hợp I / O, có lẽ bạn có thể nghĩ về nó ít hơn một đường ống được tôn vinh trong đó nhà nước toàn cầu là những gì đang được thông qua giữa các giai đoạn.

Ví dụ: hãy lấy mã bạn đang viết:

do
  putStrLn "What is your name?"
  name <- getLine
  putStrLn ("Nice to meet you, " ++ name ++ "!")

Có nhiều thứ đang diễn ra ở đây hơn là bắt mắt. Chẳng hạn, bạn sẽ nhận thấy putStrLncó chữ ký sau : putStrLn :: String -> IO (). Tại sao lại thế này?

Hãy suy nghĩ về nó theo cách này: hãy giả vờ (vì đơn giản) rằng stdout và stdin là các tệp duy nhất chúng ta có thể đọc và ghi vào. Trong một ngôn ngữ bắt buộc, điều này không có vấn đề. Nhưng trong một ngôn ngữ chức năng, bạn không thể thay đổi trạng thái toàn cầu. Hàm chỉ đơn giản là một cái gì đó lấy một giá trị (hoặc giá trị) và trả về một giá trị (hoặc giá trị). Một cách khác là sử dụng trạng thái toàn cầu làm giá trị được truyền vào và ra khỏi mỗi chức năng. Vì vậy, bạn có thể dịch dòng mã đầu tiên thành một cái gì đó như thế này:

global_state <- (\(stdin, stdout) -> (stdin, stdout ++ "What is your name?")) global_state

... và trình biên dịch sẽ biết in bất cứ thứ gì được thêm vào phần tử thứ hai của global_state. Bây giờ tôi không biết về bạn, nhưng tôi không thích lập trình như vậy. Cách thức này được thực hiện dễ dàng hơn là sử dụng Monads. Trong Monad, bạn chuyển một giá trị đại diện cho một loại trạng thái từ hành động này sang hành động tiếp theo. Đây là lý do tại sao putStrLncó kiểu trả về IO (): nó trả về trạng thái toàn cầu mới.

Vậy tại sao bạn quan tâm? Chà, những lợi thế của lập trình chức năng so với chương trình bắt buộc đã được tranh luận đến chết ở một số nơi, vì vậy tôi sẽ không trả lời câu hỏi đó nói chung (nhưng hãy xem bài viết này nếu bạn muốn nghe trường hợp lập trình chức năng). Đối với trường hợp cụ thể này, nó có thể hữu ích nếu bạn hiểu những gì Haskell đang cố gắng thực hiện.

Rất nhiều lập trình viên cảm thấy rằng Haskell cố gắng ngăn họ viết mã bắt buộc hoặc sử dụng các tác dụng phụ. Điều đó không hoàn toàn đúng. Nghĩ về nó theo cách này: một ngôn ngữ bắt buộc là một ngôn ngữ cho phép các tác dụng phụ theo mặc định, nhưng cho phép bạn viết mã chức năng nếu bạn thực sự muốn (và sẵn sàng đối phó với một số mâu thuẫn sẽ yêu cầu). Haskell hoàn toàn là chức năng theo mặc định, nhưng cho phép bạn viết mã bắt buộc nếu bạn thực sự muốn (điều này bạn làm nếu chương trình của bạn có ích). Vấn đề không phải là làm cho việc viết mã có tác dụng phụ trở nên khó khăn. Đó là để đảm bảo bạn rõ ràng về việc có tác dụng phụ (với hệ thống loại thực thi điều này).


6
Đoạn cuối đó là vàng. Để trích xuất và diễn giải nó một chút: "Ngôn ngữ bắt buộc là mặc định cho phép tác dụng phụ theo mặc định, nhưng cho phép bạn viết mã chức năng nếu bạn thực sự muốn. Ngôn ngữ chức năng hoàn toàn là chức năng theo mặc định, nhưng cho phép bạn viết mã mệnh lệnh nếu bạn thực sự muốn. "
Frank Shearar

Điều đáng chú ý là bài báo mà bạn liên kết để từ chối một cách cụ thể ý tưởng "bất biến như một ưu điểm của lập trình chức năng" ngay từ đầu.
Mason Wheeler

@MasonWheeler: Tôi đã đọc những đoạn đó, không phải là bỏ qua tầm quan trọng của tính bất biến, mà bác bỏ nó như một lý lẽ thuyết phục để chứng minh tính ưu việt của lập trình chức năng. Trên thực tế, ông nói điều tương tự về việc loại bỏ goto(như một lập luận cho lập trình có cấu trúc) một lát sau trong bài báo, mô tả các đối số như "không có kết quả". Nhưng không ai trong chúng ta thầm mong muốn gotosự trở lại. Đơn giản là bạn không thể tranh luận rằng gotokhông cần thiết cho những người sử dụng nó rộng rãi.
Robert Harvey

7

Tôi sẽ cắn!!! Bản thân các đơn vị không thực sự là một tên tội phạm cho Haskell (phiên bản đầu của Haskell thậm chí không có chúng).

Câu hỏi của bạn hơi giống như nói "C ++ khi tôi nhìn vào cú pháp, tôi rất chán. Nhưng các mẫu là một tính năng được quảng cáo cao của C ++ vì vậy tôi đã xem xét một triển khai bằng một số ngôn ngữ khác".

Sự phát triển của một lập trình viên Haskell là một trò đùa, nó không có nghĩa là phải nghiêm túc.

Một Monad cho mục đích của một chương trình trong Haskell là một thể hiện của loại Monad, nghĩa là, đó là một loại xảy ra để hỗ trợ một nhóm hoạt động nhỏ nhất định. Haskell có hỗ trợ đặc biệt cho các loại thực hiện lớp loại Monad, đặc biệt là hỗ trợ cú pháp. Thực tế những gì kết quả này là những gì đã được gọi là "dấu chấm phẩy lập trình". Khi bạn kết hợp chức năng này với một số tính năng khác của Haskell (chức năng hạng nhất, sự lười biếng theo mặc định), điều bạn nhận được là khả năng thực hiện một số điều như các thư viện thường được coi là các tính năng ngôn ngữ. Bạn có thể, ví dụ, thực hiện một cơ chế ngoại lệ. Bạn có thể thực hiện hỗ trợ cho các phần tiếp theo và coroutines như một thư viện. Haskell, ngôn ngữ không hỗ trợ cho các biến có thể thay đổi:

Bạn hỏi về "Có thể / Danh tính / Đơn vị phân chia an toàn ???". Đơn vị có thể là một ví dụ về cách bạn có thể thực hiện (rất đơn giản, chỉ có một ngoại lệ) xử lý ngoại lệ như một thư viện.

Bạn nói đúng, viết tin nhắn và đọc thông tin người dùng không phải là rất độc đáo. IO là một ví dụ tệ hại của "đơn nguyên như một tính năng".

Nhưng để lặp lại, một "tính năng" của riêng nó (ví dụ: Monads) tách biệt với phần còn lại của ngôn ngữ không nhất thiết phải xuất hiện ngay lập tức (một tính năng mới tuyệt vời của C ++ 0x là tham chiếu giá trị, không có nghĩa là bạn có thể sử dụng Chúng nằm ngoài ngữ cảnh C ++ vì cú pháp của nó làm bạn khó chịu và nhất thiết phải thấy tiện ích này). Ngôn ngữ lập trình không phải là thứ bạn có được bằng cách ném một loạt các tính năng vào một cái xô.


Trên thực tế, haskell có hỗ trợ cho các biến có thể thay đổi thông qua đơn vị ST (một trong số ít các phần ma thuật không tinh khiết kỳ lạ của ngôn ngữ chơi theo quy tắc riêng của nó).
sara

4

Các lập trình viên đều viết chương trình, nhưng sự tương đồng kết thúc ở đó. Tôi nghĩ rằng các lập trình viên khác nhau nhiều hơn nhiều so với hầu hết các lập trình viên có thể tưởng tượng. Thực hiện bất kỳ "trận chiến" lâu dài nào, như gõ biến tĩnh so với các loại chỉ chạy trong thời gian chạy, kịch bản so với biên dịch, kiểu C so với hướng đối tượng. Bạn sẽ thấy không thể tranh luận một cách hợp lý rằng một trại kém hơn, bởi vì một số trong số họ tạo ra mã xuất sắc trong một số hệ thống lập trình dường như vô nghĩa hoặc thậm chí hoàn toàn không thể sử dụng được đối với tôi.

Tôi nghĩ rằng những người khác nhau chỉ nghĩ khác nhau, và nếu bạn không bị cám dỗ bởi cú pháp cú pháp hay đặc biệt là trừu tượng chỉ tồn tại cho thuận tiện và thực sự có chi phí thời gian chạy đáng kể, thì bằng mọi cách hãy tránh xa những ngôn ngữ đó.

Tuy nhiên, tôi khuyên bạn ít nhất nên cố gắng làm quen với các khái niệm mà bạn đang từ bỏ. Tôi không có gì chống lại ai đó kịch liệt ủng hộ C, miễn là họ thực sự hiểu vấn đề lớn về biểu hiện lambda là gì. Tôi nghi ngờ rằng hầu hết sẽ không ngay lập tức trở thành một người hâm mộ, nhưng ít nhất nó sẽ ở đó trong tâm trí họ khi họ tìm thấy vấn đề hoàn hảo, điều sẽ trở nên dễ dàng hơn rất nhiều để giải quyết với lambdas.

Và trên hết, hãy cố gắng tránh bị làm phiền bởi những người hâm mộ nói, đặc biệt là bởi những người không thực sự biết họ đang nói gì.


4

Haskell thi hành tính minh bạch tham chiếu : được cung cấp cùng một tham số, mọi hàm luôn trả về cùng một kết quả, bất kể bạn gọi hàm đó bao nhiêu lần.

Điều đó có nghĩa là, ví dụ, trên Haskell (và không có Monads), bạn không thể thực hiện trình tạo số ngẫu nhiên. Trong C ++ hoặc Java, bạn có thể thực hiện điều đó bằng cách sử dụng các biến toàn cục, lưu trữ giá trị "hạt giống" trung gian của trình tạo ngẫu nhiên.

Trên Haskell, bản sao của các biến toàn cục là Monads.


Vậy ... nếu bạn muốn một trình tạo số ngẫu nhiên thì sao? Nó không phải là một chức năng là tốt? Thậm chí nếu không, làm thế nào để tôi có được một trình tạo số ngẫu nhiên?
Công việc

@Job Bạn có thể tạo một trình tạo số ngẫu nhiên bên trong một đơn nguyên (về cơ bản là trình theo dõi trạng thái) hoặc bạn có thể sử dụng unsafePerformIO, ác quỷ của Haskell không bao giờ được sử dụng (và thực tế có thể sẽ phá vỡ chương trình của bạn nếu bạn sử dụng ngẫu nhiên bên trong nó!)
thay thế

Trong Haskell, bạn có thể vượt qua 'RandGen', về cơ bản là trạng thái hiện tại của RNG. Vì vậy, hàm tạo ra một số ngẫu nhiên mới sẽ lấy RandGen và trả về một tuple với RandGen mới và số được tạo. Cách khác là chỉ định một nơi nào đó mà bạn muốn có một danh sách các số ngẫu nhiên giữa giá trị tối thiểu và tối đa. Điều này sẽ trả về một dòng số vô hạn được đánh giá một cách lười biếng, vì vậy chúng ta chỉ cần đi qua danh sách này bất cứ khi nào chúng ta cần một số ngẫu nhiên mới.
Qqwy

Giống như cách bạn có được chúng trong bất kỳ ngôn ngữ nào khác! bạn nắm giữ một số thuật toán tạo số giả ngẫu nhiên, và sau đó bạn gieo nó với một số giá trị và gặt hái các số "ngẫu nhiên" xuất hiện! Sự khác biệt duy nhất là các ngôn ngữ như C # và Java sẽ tự động tạo PRNG cho bạn bằng cách sử dụng đồng hồ hệ thống hoặc những thứ tương tự. Điều đó và thực tế là trong haskell, bạn cũng nhận được một PRNG mới mà bạn có thể sử dụng để lấy số "tiếp theo", trong khi trong C # / Java, tất cả đều được thực hiện bằng cách sử dụng các biến có thể thay đổi trong Randomđối tượng.
sara

4

Đây là một câu hỏi cũ nhưng nó thực sự là một câu hỏi hay, vì vậy tôi sẽ trả lời.

Bạn có thể nghĩ các đơn nguyên là các khối mã mà bạn có toàn quyền kiểm soát cách chúng được thực thi: mỗi dòng mã sẽ trả về, liệu việc thực thi có dừng lại tại bất kỳ điểm nào hay không, liệu một số xử lý khác có xảy ra giữa mỗi dòng không.

Tôi sẽ đưa ra một số ví dụ về những điều mà các đơn vị cho phép sẽ khó khăn nếu không. Không có ví dụ nào trong Haskell, chỉ vì kiến ​​thức về Haskell của tôi hơi run rẩy, nhưng chúng đều là những ví dụ về cách Haskell đã truyền cảm hứng cho việc sử dụng các đơn vị.

Trình phân tích cú pháp

Thông thường, nếu bạn muốn viết một trình phân tích cú pháp nào đó, giả sử để thực hiện ngôn ngữ lập trình, bạn sẽ phải đọc đặc tả BNF và viết một loạt mã mã để phân tích cú pháp, hoặc bạn sẽ phải sử dụng trình biên dịch trình biên dịch như Flex, Bison, yacc, v.v. Nhưng với các đơn nguyên, bạn có thể tạo một loại "trình phân tích cú pháp trình biên dịch" ngay trong Haskell.

Trình phân tích cú pháp thực sự không thể được thực hiện mà không có ngôn ngữ đơn nguyên hoặc ngôn ngữ đặc biệt như yacc, bison, v.v.

Chẳng hạn, tôi lấy đặc tả ngôn ngữ BNF cho giao thức IRC :

message    =  [ ":" prefix SPACE ] command [ params ] crlf
prefix     =  servername / ( nickname [ [ "!" user ] "@" host ] )
command    =  1*letter / 3digit
params     =  *14( SPACE middle ) [ SPACE ":" trailing ]
           =/ 14( SPACE middle ) [ SPACE [ ":" ] trailing ]

nospcrlfcl =  %x01-09 / %x0B-0C / %x0E-1F / %x21-39 / %x3B-FF
                ; any octet except NUL, CR, LF, " " and ":"
middle     =  nospcrlfcl *( ":" / nospcrlfcl )
trailing   =  *( ":" / " " / nospcrlfcl )

SPACE      =  %x20        ; space character
crlf       =  %x0D %x0A   ; "carriage return" "linefeed"

Và giảm xuống còn khoảng 40 dòng mã trong F # (một ngôn ngữ khác hỗ trợ các đơn nguyên):

type UserIdentifier = { Name : string; User: string; Host: string }

type Message = { Prefix : UserIdentifier option; Command : string; Params : string list }

let space = character (char 0x20)

let parameters =
    let middle = parser {
        let! c = sat <| fun c -> c <> ':' && c <> (char 0x20)
        let! cs = many <| sat ((<>)(char 0x20))
        return (c::cs)
    }
    let trailing = many item
    let parameter = prefixed space ((prefixed (character ':') trailing) +++ middle)
    many parameter

let command = atLeastOne letter +++ (count 3 digit)

let prefix = parser {
    let! name = many <| sat (fun c -> c <> '!' && c <> '@' && c <> (char 0x20))   //this is more lenient than RFC2812 2.3.1
    let! uh = parser {
        let! user = maybe <| prefixed (character '!') (many <| sat (fun c -> c <> '@' && c <> (char 0x20)))
        let! host = maybe <| prefixed (character '@') (many <| sat ((<>) ' '))
        return (user, host)
    }
    let nullstr = function | Some([]) -> null | Some(s) -> charsString s | _ -> null
    return { Name = charsString name; User = nullstr (fst uh); Host = nullstr (snd uh) }
}

let message = parser {
    let! p = maybe (parser {
        let! _ = character ':'
        let! p = prefix
        let! _ = space
        return p
    })
    let! c = command
    let! ps = parameters
    return { Prefix = p; Command = charsString c; Params = List.map charsString ps }
}

Cú pháp đơn nguyên của F # khá xấu so với Haskell và tôi có thể đã cải thiện điều này khá nhiều - nhưng điểm đáng chú ý là về mặt cấu trúc, mã trình phân tích cú pháp giống hệt với BNF. Điều này không chỉ mất nhiều công sức hơn nếu không có các đơn vị (hoặc trình tạo trình phân tích cú pháp), mà nó gần như không giống với đặc điểm kỹ thuật, và do đó thật tồi tệ khi đọc và duy trì.

Đa nhiệm tùy chỉnh

Thông thường, đa nhiệm được coi là một tính năng của hệ điều hành - nhưng với các đơn nguyên, bạn có thể viết lịch trình của riêng mình để sau mỗi đơn vị hướng dẫn, chương trình sẽ chuyển quyền điều khiển sang trình lập lịch, sau đó sẽ chọn một đơn vị khác để thực thi.

Một anh chàng đã tạo ra một "nhiệm vụ" đơn vị để điều khiển các vòng lặp trò chơi (một lần nữa trong F #), để thay vì phải viết mọi thứ như một máy trạng thái hoạt động trên mỗi Update()cuộc gọi, anh ta chỉ có thể viết tất cả các hướng dẫn như thể chúng là một chức năng duy nhất .

Nói cách khác, thay vì phải làm một cái gì đó như:

class Robot
{
   enum State { Walking, Shooting, Stopped }

   State state = State.Stopped;

   public void Update()
   {
      switch(state)
      {
         case State.Stopped:
            Walk();
            state = State.Walking;
            break;
         case State.Walking:
            if (enemyInSight)
            {
               Shoot();
               state = State.Shooting;
            }
            break;
      }
   }
}

Bạn có thể làm một cái gì đó như:

let robotActions = task {
   while (not enemyInSight) do
      Walk()
   while (enemyInSight) do
      Shoot()
}

LINQ to SQL

LINQ to SQL thực sự là một ví dụ về một đơn nguyên và chức năng tương tự có thể dễ dàng được thực hiện trong Haskell.

Tôi sẽ không đi vào chi tiết vì tôi không nhớ chính xác tất cả, nhưng Erik Meijer giải thích điều đó khá rõ .


1

Nếu bạn đã quen thuộc với các mẫu GoF, các đơn nguyên giống như mẫu Trang trí và mẫu Trình tạo được đặt cùng nhau, trên các steroid, bị cắn bởi một kẻ xấu phóng xạ.

Có những câu trả lời tốt hơn ở trên, nhưng một số lợi ích cụ thể tôi thấy là:

  • monads trang trí một số loại lõi với các thuộc tính bổ sung mà không thay đổi loại lõi. Ví dụ: một đơn nguyên có thể "nâng" Chuỗi và thêm các giá trị như "isWellFormed", "isProfanity" hoặc "isPalindrom", v.v.

  • tương tự, các đơn nguyên cho phép kết hợp một loại đơn giản thành một loại bộ sưu tập

  • các đơn vị cho phép liên kết muộn các chức năng vào không gian bậc cao này

  • các đơn vị cho phép trộn các hàm và đối số tùy ý với một kiểu dữ liệu tùy ý, trong không gian bậc cao hơn

  • các đơn vị cho phép pha trộn các chức năng thuần túy, không trạng thái với một cơ sở không tinh khiết, có trạng thái, vì vậy bạn có thể theo dõi nơi xảy ra sự cố

Một ví dụ quen thuộc của một đơn nguyên trong Java là Danh sách. Nó nhận một số lớp lõi, như String và "nâng" nó vào không gian đơn nguyên của Danh sách, thêm thông tin về danh sách. Sau đó, nó liên kết các hàm mới vào không gian đó như get (), getFirst (), add (), blank (), v.v.

Ở quy mô lớn, hãy tưởng tượng rằng thay vì viết một chương trình, bạn chỉ cần viết một Builder lớn (như mẫu GoF) và phương thức build () cuối cùng đưa ra bất kỳ câu trả lời nào mà chương trình sẽ tạo ra. Và rằng bạn có thể thêm các phương thức mới vào ProgramBuilder mà không cần biên dịch lại mã gốc. Đó là lý do tại sao các đơn vị là một mô hình thiết kế mạnh mẽ.

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.