[英]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 ++ "!
其工作方式是getLine
是IO 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 <- something
, something
類型又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
。
我認為您已經理解了這一點,因為您隨后使用Left
和Right
在tehfile
進行了模式匹配,鑒於其類型,這是完全正確的。 但是,您接下來要做的事情並沒有真正意義,並且會導致編譯器錯誤。
基本上,如果文件被成功打開和解析, 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
以進行編譯(這只是從String
到Text
的轉換,不要讓我開始了解 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.