我做 Haskell 已經有一段時間了(小專案 ~ 3K LOC),我仍然覺得自己像個新手。我不相信我有一個好的 Haskell 風格;我通常會選擇地圖/過濾器/折疊。沒有花哨的單子/應用程式等。
我想改進。我有一個簡單的要求來生成 377 兆赫的次諧波,并以 8 列(任意)的表格形式列印出來,所以我用三種方式撰寫了它。(我知道我可以使用“盒子”包,但這對我來說是一個練習)。
我真的很想得到關于“首選”或另一種更“Haskell”的不同方式的反饋。(我發現串列理解最困難,因為我試圖在沒有“地圖”的情況下做到這一點)
我為自己感到自豪..我第一次使用應用程式!
評論贊賞,包括我可以看到好的 Haskell 風格的地方。我看過大型包(即 Megaparsec),它們使用技巧和語言擴展,這對我來說很難理解。我希望最終能夠理解它們,但現在它是壓倒性的。
謝謝!
湯姆
import Data.List (intercalate)
import Text.Printf
import Data.List.Split (chunksOf)
gen :: [Float]
gen = pure (/) <*> [377] <*> [2,3..50]
main :: IO()
main = do
-- Try One -- ... List function
let ps = map (\f -> printf "%7.2f\t" f) gen
putStr $ concat (intercalate ["\n"] (chunksOf 8 ps))
putStr "\n"
-- Try Two -- ... IO Map
mapM_ (\xs -> (mapM_ (\x -> printf "%7.2f\t" x) xs)
>> (printf "\n")) (chunksOf 8 gen)
-- Try Three -- ... List Comprehension
putStr $ concat [ ys' | ys <- (chunksOf 8 gen),
ys' <- (map (\y ->
printf "%7.2f\t" y) ys) ["\n"] ]
uj5u.com熱心網友回復:
我是 Applicative 風格的粉絲,但即使是我也會
gen寫成map (377 /) [2..50].三個版本都寫
(\x -> printf "%7.2f\t" x),這只是一種更復雜的寫法(printf "%7.2f\t")。需要記住的 lambda 引數越少越好。版本 1 和 3 中的大部分作業都在努力在字串中間插入換行符。但是已經有一個庫函式:
unlines. 有了這個函式,整個問題就很簡單了:main = putStrLn . unlines . map renderRow . chunksOf 8 $ gen where renderRow = concatMap (printf "%7.2f\t")為其他問題節省你的腦力。
我不喜歡第 2 版:它將 IO 和純代碼交錯,而第 1 版和第 3 版則很好地留在了純世界中。但是,如果您打算這樣做,我會使用
for_anddo而不是mapM_:for_ (chunksOf 8 gen) $ \line -> do mapM_ (printf "%7.2f\t") line putStr "\n"我認為縮進和行分隔符有助于使結構明顯,與括號和
mapM_. 我個人的經驗法則是我不喜歡將 lambdas 傳遞給map家庭中的函式。它對于現有函式或部分應用程式非常有用,但如果您必須撰寫一個 lambda,您可能最好使用串列理解、來自for家庭的東西或其他替代方法。版本 3 看起來不錯,但是
map在串列理解系結中間看到 a 很奇怪。需要考慮的一件事:如果您難以撰寫串列推導式,do不妨試一試。我知道我個人在撰寫串列推導時遇到了麻煩,因為我必須先撰寫“正文”,然后才能弄清楚我要系結哪些變數。所以[f y | x <- whatever, y <- something x]我通常寫而不是do x <- whatever y <- something x pure $ f y這使得過濾條件變得更加麻煩——你必須匯入
Control.Monad.guard——但我喜歡不那么密集的布局和相反的順序。
uj5u.com熱心網友回復:
改善風格的一種方法是養成積極重構代碼的習慣,利用管理某些操作的“法則”,并始終尋找base可能取代您手工編碼的東西的現有功能(尤其是 . 這不會自動導致更好的風格,并且在某些情況下可能會適得其反。但是,它將幫助您練習以良好風格撰寫 Haskell 代碼所需的技能(或者,更重要的是,采用您最初以不良風格撰寫的 Haskell 代碼并改進它。)
例如,您的應用程式操作:
gen = pure (/) <*> [377] <*> [2,3..50]
將pure函式應用于應用引數。根據應用法則,這可以簡化為運算子的使用<$>( 的運算子版本fmap):
gen = (/) <$> [337] <*> [2,3..50]
你的第一個論點,[337]其實也很純粹。對于“串列”應用程式,純值是單例。因此,這可以重構為:
gen = (/) <$> pure 337 <*> [2,3..50]
這是可讀性的倒退,但也<*>可以用純部分應用函式替換純函式的應用程式(即 )。換句話說,適用法律意味著:
f <$> pure x <*> ... = pure f <*> pure x <*> ... = pure (f x) <$> ... = f x <$> ...
所以,我們有:
gen = (/) 337 <$> [2,3..50]
或者,使用“部分”:
gen = (337 /) <$> [2,3..50]
這是好風格嗎?我不知道。也許串列理解更好:
gen = [337 / n | n <- [2,3..50]]
但我確實認為其中任何一個都比原來的要好:
gen = pure (/) <*> [377] <*> [2,3..50]
不是因為原來的風格很糟糕,而是因為這兩種選擇在語意上是等效的,同時更容易閱讀和/或理解,這應該是編程風格的主要目標之一。
上面,我已經說得好像你必須牢記所有這些復雜的“法則”并有意識地應用它們,使重構成為一個乏味且容易出錯的程序。但是,由于大量的應用重構實踐,我發現這種轉換是完全自動的。我重寫了:
gen = pure (/) <*> [377] <*> [2,3..50]
至:
gen = (337 /) <$> [2,3..50]
一步,因為這對我來說是完全顯而易見的,就像任何花一點時間重構應用運算式的 Haskell 程式員一樣。好吧,好吧...從技術上講,我首先將其重寫為:
gen = (/ 337) <$> [2,3..50]
但很快糾正了我的錯誤。此外,我最終意識到步長為 1,導致我將串列從 重寫[2,3..50]為[2..50]。可以說這不是更具可讀性,但第一個版本可能會向有經驗的 Haskell 程式員建議使用除 1 以外的步長,從而引起一些混亂(就像它對我所做的那樣)。
類似的管理函陣列合的“法則”允許您采用如下代碼:
-- Try One -- ... List function
let ps = map (\f -> printf "%7.2f\t" f) gen
putStr $ concat (intercalate ["\n"] (chunksOf 8 ps))
putStr "\n"
并立即將其重構為:
putStr $ concat . intercalate ["\n"] . chunksOf 8 . map (printf "%7.2f\t") $ gen
putStr "\n"
并且對庫函式的一些了解使您可以進一步重構為:
putStr $ unlines . map concat . chunksOf 8 . map (printf "%7.2f\t") $ gen
甚至:
putStr $ unlines . map (concatMap (printf "%7.2f\t")) . chunksOf 8 $ gen
甚至:
putStr $ unlines . (map . concatMap . printf) "%7.2f\t" . chunksOf 8 $ harmonics
并非所有這些重構都會帶來更好的風格。例如,最后一個可能只應該用作一個笑話。但是,當您嘗試改進特定代碼塊的樣式時,這種對 Haskell 程式的操作是了解在更現實的程式中可以使用哪些選項的先決條件。
此外,在重構時,您需要時不時地退后一步,考慮是否不能以更簡單的方式重新構想您執行的大型復雜轉換。
我查看了您的解決方案#1,并以這種方式思考:
- 制表符是魔鬼的工具,輸出只能使用空格。
printf格式化 8 個字符的單元格和 7 個字符的單元格一樣- 要獲得 8 列的行,我們可以將所有內容粉碎在一起,而不是一次抓取 64 個字符的行。
- 有一個
unlines庫函式可以從行串列中生成多行字串。
所以:
main = putStr $ unlines . chunksOf (8*8) .
concatMap (printf "%8.2f") . map (337 /) $ [2,3..50:Double]
這是好風格嗎?也許是為了快速編程練習,但對于真正的代碼,強調“不,這很糟糕”。在生產代碼中,我可能會寫:
table :: Int -> Int -> [Double] -> String
table cols cellwidth = unlines . chunksOf linesize . concatMap cell
where linesize = cols*cellwidth
cell = printf "%*.2f" cellwidth
harmonics :: [Double]
harmonics = map (337 /) [2..50]
main = putStr $ table 8 8 harmonics
這清楚地將資料的生成與harmonics排版 intable與 IO in區分開來putStr。它還通過使它們具有可理解的名稱的顯式引數來擺脫表代碼中的所有魔術常量。再想一想之后,我可能會認為“將所有內容粉碎在一起并使用固定的行長”太過分了,所以可能:
-- Typeset list of values into table.
table :: Int -> Int -> [Double] -> String
table cols cellwidth = unlines . map (concatMap cell) . chunksOf cols
where cell = printf "%*.2f" cellwidth
更好,即使操作map (concatMap cell)是一個令人困惑的操作。table然而,如果有一個合理的評論并且它的論點有好聽的名字,這可能并不重要。
無論如何,重點是即使不是所有的重構都會自動產生好的風格,但重構技巧對于始終如一地寫出好的風格是絕對必要的。
關于尋找優秀 Haskell 風格的來源,我在學習 Haskell 時發現,撰寫良好的教程和博客文章是很好的來源,即使它們不是專門關于風格的。例如,Write You a Scheme和Intro to Parsing with Parsec in Haskell都以良好的風格撰寫了 Haskell 源代碼。它們還有一個優勢,就是針對那些不知道在現實世界中的 Haskell 庫中發現的技巧和語言擴展的人,而且你會學到一些非常有用和有趣的東西(例如,如何撰寫解釋器和Parsec 決議器)同時采用他們的好風格。
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/517164.html
標籤:哈斯克尔
上一篇:計算字串中的出現次數
