簡體   English   中英

在 Haskell IO() 主函數中返回值

[英]Returning values in Haskell IO() main function

有一個看起來像這樣的腳本:

main :: IO ()
main = do
  tehfile <- readIniFile "somefile"

  z <- case tehfile of
    Left a -> 42
    Right b -> print "bar"

  x <- case tehfile of
    Left a -> print "foo"
    Right b -> print "bar"

  return ()

我收到諸如"No instance for (Num (IO ())) arising from the literal '42'" 這是非常神秘的,但是將值 42 更改為不同的類型我已經能夠確定函數 main - IO()的類型與 42 的類型不匹配,Num。

我的問題是,為什么盡管模式匹配的結果存儲在變量z並且顯式返回 a ()而不是z ,但我仍然收到類型錯誤? 在 Scala 中,如果沒有return ()x將按照約定返回。 Haskell 的這種行為真的讓我感到困惑,因為我只能將其解釋為main:IO()所有變量都需要是IO()類型。 我對么?

這個答案主要基於您的評論,我在下面引用了該評論,以免丟失。 (並且為它的長度道歉:我真的不打算讓它變得如此龐大,但我覺得有很多話會有所幫助。希望是這樣,但請不要猶豫告訴我不是。)

Read INI file, read section list off it, print it into stdout, ask for user to choose one of them(simple getline is fine) and then set the selected section as [default]. Pretty much. My main struggle is constant type error when I store results of functions perfroming what I want in main.

這是您鏈接到的代碼:

module Main where
import Data.Ini

main :: IO ()
main = do
  tehfile <- readIniFile "/Users/g.reshetniak/.aws/credentials" -- ~/.aws/credentials: openFile: does not exist --> why shell expansion doesn't work?

  s <- case tehfile of
    Left a -> print "woot"
    Right b -> head (map (\x -> print x) (sections b)) --TODO why this prints one element instead of mapping print over all section

  --TODO selected_profile <- getLine
  --TODO why `selected_profile <- getLine` is different from `let selected_profile = getLine` ?

  p <- case tehfile of
    Left c -> print "oops" --TODO why this does not print "oops" on lacking profile, instead prints Left "Couldn't find section: selected_profile"
    Right d -> print (lookupValue "selected_profile" "aws_access_key_id" d)
  return ()

現在我必須承認我以前從未遇到過“INI 文件”,所以我不熟悉這種格式。 所以其中一些是基於我可以從您正在使用的Data.Ini模塊的文檔中收集到的信息(平心而論,這似乎非常簡單)。 所以我不能保證向你展示如何做你想要的(特別是我真的不知道你所說的“將所選部分設置為[默認]”是什么意思) - 但我希望回答一些問題您已經在代碼注釋中提出了問題,並且可能糾正了您對在 Haskell 中執行 I/O 的一些誤解。

我從這個文件中注意到的第一件事,就像原始問題中的代碼一樣,是您使用<-運算符將變量綁定到值,但不使用它們。 在 Haskell 的do塊中執行s <- someValue有點像在命令式語言中執行s = someValue 如果您之后不使用s變量,那么在命令式語言中這樣做是毫無意義的,而在 Haskell 中它也完全沒有意義,原因完全相同。 換句話說,這:

main = do
    ....
    s <- someValue
    ....

是,如果第二個....沒有在任何地方提到s ,完全等同於:

main = do
    ....
    someValue
    ....

請注意, someValue已保留在那里,因為它可能具有一些對程序很重要的副作用 - 但無論該操作返回的“結果”如何,如果有的話,都是不需要的。

例如,這里是 Haskell 中最簡單的控制台應用程序,該程序會詢問您的姓名,然后使用它向您致意:

main = do
    putStrLn "Please tell me your name"
    name <- getLine
    putStrLn $ "Welcome, " ++ name ++ "!

其工作方式是getLineIO String類型的“IO 操作”。 它基本上相當於Python的input函數(更准確地說, name <- getLine相當於Python中的name = input() ):它是一個“動作”,執行時從控制台讀取一行輸入,然后“返回”用戶輸入的字符串。 如果您想訪問該字符串,您必須按照上述操作並將其綁定到帶有<- “operator” 的變量。 (它不是真正的運算符——它實際上是一種特殊的語法糖,但現在不用擔心,如果你願意,你可以把它想象成一個運算符。)但是,如果你只是想向用戶打招呼每次都以相同的方式,那么您就不需要綁定結果,只需執行以下操作:

main = do
    putStrLn "Please tell me your name"
    getLine
    putStrLn "Welcome!"

即使程序完全忽略它,它仍然給用戶輸入他們名字的樂趣。

通常,當您在main或任何其他IO操作中有do塊時,每一行都必須是IO a類型的表達式,其中a可以是任何類型(只有最后一行必須具有“正確”類型對於a - 通常是IO ()main的情況下)。 當您執行x <- somethingsomething類型又something IO a ,然后 x 的類型將是a :它是您作為 IO 操作的結果得到的任何something ,它保證是a類型a因為something具有輸入IO a

所以當你這樣做時,在你的程序開始時:

tehfile <- readIniFile "somefile"

我們從文檔中注意到readIniFile類型為FilePath -> IO (Either String Ini) ,我們看到readIniFile "somefile"類型為IO (Either String Ini) ,因此tehFile類型為Either String Ini

我認為您已經理解了這一點,因為您隨后使用LeftRighttehfile進行了模式匹配,鑒於其類型,這是完全正確的。 但是,您接下來要做的事情並沒有真正意義,並且會導致編譯器錯誤。

基本上,如果文件被成功打開和解析, tehFile將是一個Right值,其中包含一個Ini類型的值,這似乎是文件中數據的表示。 如果出現任何問題,您將獲得一個Left值,其中包含一個String ,我認為這將是某種解釋出了什么問題的錯誤消息。

你當然可以這樣做:

case tehfile of
    Left e -> print $ "an error occurred: " ++ e
    ....

為了向用戶顯示任何出錯的細節。 但請注意, case表達式正是一個表達式,因此必須有一個類型。 特別是,所有分支的結果——這里是Left分支和Right分支——必須具有相同的類型。 print接受某種可打印類型的值並輸出IO () 因此, Right b分支還必須輸出IO ()類型的值 - 換句話說,它必須像print "woot" ,是可能執行某些 IO 但沒有有意義的“結果”的操作。

你實際上已經這樣做了:

head (map (\x -> print x) (sections b))

我會寫得更簡潔易讀(但完全等效),如

head $ map print (sections b)

這是完全類型正確的: sections b是一個Text值列表(本質上是字符串), print將每個值轉換為IO ()類型的值(帶有打印值的副作用),然后head取第一個元素。 所以上面的表達式有類型IO () ,根據你的需要 - 它是一個 IO 操作,它只是打印“部分”值的第一個。 您的評論表明您對僅打印第一個元素感到困惑:這是 Haskell 懶惰評估的結果。 在“熱切”的語言中, map print (sections b)會打印所有這些值,但在這里您只需要這些值中的第一個,因此實際上只打印了第一個部分。

當然,正如您可能注意到的那樣,簡單地執行輸入是不正確的

Right b -> map print (sections b)

因為右邊的表達式的類型為[IO ()]並且您需要一個IO () 這就是mapM_函數的用途:

Right b -> mapM_ print (sections b)

在可觀察效果方面會做完全相同的事情,但類型現在是您需要的IO () [它是mapM一個變體,它運行在一個值列表上,並使用一個函數從每個值中進行 IO 操作,而不是像map那樣返回操作列表,而是返回一個結果為列表的操作每個單獨動作的相應結果。 mapM_是相同的,但會“丟棄”結果,因此結果不是IO [a]形式,而是IO () ,正如您在此處需要的那樣。 還要注意,這兩個函數都比我在這里解釋的更通用,但這就是它們與列表和 IO 一起工作的方式,這是它們的一個非常常見的用例。]

把它放在一起,對於這段代碼,你將擁有:

case tehfile of
    Left e -> print $ "an error occurred: " ++ e
    Right b -> mapM_ print (sections b)

並注意我故意省略了s <-在它之前,它可以在您的代碼中找到。 這樣做是完全有效的,但您不僅不會在以后的代碼中s任何地方使用s (使<-像我上面解釋的那樣無用),而且您甚至絕對無法使用它。 case表達式的類型為IO () ,因此任何綁定到結果的內容都將具有類型() - 而()是一種只有一個值的類型。 這就是為什么我們對像main這樣沒有合理結果值的操作使用IO ()的原因,因為類型系統要求我們在這里使用某種類型,並且使用一種本質上告訴我們沒有任何意義的類型,如果沒有任何明智的去那里。

是的,我已經走了很長時間了。 希望你從上面得到了一些東西。 在我簽字之前,我將嘗試做一些簡單的事情。 首先,回答您在評論中提出的其他問題:

why selected_profile <- getLine` is different from `let selected_profile = getLine` ?

let selected_profile = getLine基本上只是一個局部變量賦值。 在這種情況下,它使selected_profile等於getLine ,這是IO String類型的操作。 它實際上並不“執行”任何 IO,而只是允許您以新名稱使用getLine IO 操作 - 回到 Python,就像在該語言中執行selected_profile = input一樣; 該行本身不會要求用戶提供任何東西,直到您執行該功能才會發生這種情況。 Haskell 中的getLine不是函數,而是一個“動作”(由運行時執行,而不是在 Haskell 代碼中執行),但原理是相同的。 selected_profile <- getLine是實際運行getLine操作的對象,以詢問用戶輸入,然后將其分配給局部變量selected_profile

why this does not print "oops" on lacking profile, instead prints Left "Couldn't find section: selected_profile"

這是因為,在更廣泛的代碼上下文中,您成功打開並解析了 INI 文件,因此tehfile保存了Right值。 看來要打印“哎呀”,如果用戶對所選配置文件的輸入不能在文件中找到,但為了做到這一點,你需要運行lookUpValue第一功能,其結果模式匹配,以在Left (錯誤)值的情況下打印“oops”。

因此,為了完善,這里有一個非常簡單的main版本,它至少可以非常粗略地按照您的意圖執行。 請注意,您需要從Data.Text導入pack以進行編譯(這只是從StringText的轉換,不要讓我開始了解 Haskell 中可笑的字符串類型數量以及為什么是默認類型, String ,太糟糕了,沒有嚴肅的應用程序使用它,但大多數標准庫函數仍然強迫你):

main :: IO ()
main = do
  tehfile <- readIniFile "/Users/g.reshetniak/.aws/credentials" 

  case tehfile of
    Left e -> print $ "Failed to parse file: " ++ e
    Right i -> do
       mapM_ print (sections b)
       putStrLn "Please select which section you would like"
       selected_profile <- getLine
       case lookupValue (pack selected_profile) "aws_access_key_id" i of
           Left e -> putStrLn $ "oops, an error: " ++ e
           Right v -> print v

(請注意,我沒有嘗試編譯或運行上面的代碼,所以如果發生任何意外,請告訴我。每當我一次編寫超過 4 行的 Haskell 代碼時,我似乎總是有一些愚蠢但很容易修復的錯誤...)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM