cv.cm

Pemrograman · Bahasa pemrograman

Tutorial Haskell untuk pemula

7 menit baca

Jawaban singkat

Haskell adalah bahasa fungsional murni bertipe statis dengan inferensi tipe dan lazy evaluation. Program disusun dari fungsi murni, algebraic data type, dan pattern matching, sedangkan efek samping dilacak lewat tipe seperti IO. Halaman ini membahas cara memasang GHC, tipe, fungsi dan guard, list dan fungsi tingkat tinggi, tipe data, type class, Maybe dan Either, serta satu contoh lengkap.

Apa itu Haskell dan untuk apa dipakai

Haskell adalah bahasa pemrograman fungsional murni dengan sistem tipe statis yang kuat, inferensi tipe, dan lazy evaluation. Haskell dirancang oleh sebuah komite peneliti dan pertama kali didefinisikan pada 1990; standar saat ini adalah Haskell 2010, dan GHC (Glasgow Haskell Compiler) adalah implementasi utamanya. Di Haskell, fungsi tidak memiliki efek samping kecuali tipenya menyatakan demikian, sehingga program lebih mudah dipahami dan diuji.

Haskell dipakai di bidang keuangan dan fintech, compiler dan tool bahasa pemrograman (Pandoc dan linter ShellCheck ditulis dalam Haskell), proyek blockchain, backend web, dan riset. Banyak orang juga mempelajarinya karena gagasannya, seperti algebraic data type, type class, dan fungsi murni, telah memengaruhi Rust, Swift, Scala, Kotlin, dan TypeScript.

Memasang GHC dan menjalankan Hello, world

Installer yang direkomendasikan adalah GHCup, yang memasang GHC, build tool Cabal dan Stack, serta Haskell Language Server untuk editor. Setiap program memiliki action main bertipe IO ().

-- hello.hs
main :: IO ()
main = putStrLn "Hello, world!"
runghc hello.hs          # run without compiling
# Hello, world!
ghc hello.hs && ./hello  # compile to a native executable
ghci                     # interactive REPL; load a file with :load hello.hs

Komentar diawali dengan --. Di GHCi, :type expression (atau :t) menampilkan tipe dari ekspresi apa pun, yang merupakan salah satu cara terbaik untuk belajar.

Nilai, tipe, dan inferensi tipe

Nama diikat sekali dan tidak pernah berubah: tidak ada variabel yang dapat diubah (mutable). Type signature ditulis dengan :: dan bersifat opsional karena compiler dapat menyimpulkannya, tetapi menuliskannya untuk definisi tingkat atas adalah praktik yang baik.

name :: String
name = "Haskell"

year :: Int
year = 1990

ratio :: Double
ratio = fromIntegral year / 1000   -- convert Int to Double explicitly

main :: IO ()
main = do
  putStrLn (name ++ " " ++ show year)  -- Haskell 1990
  print ratio                          -- 1.99
  print (7 `div` 2, 7 `mod` 2)         -- (3,1)
  print (2 ^ 64 :: Integer)            -- 18446744073709551616
TipeContohCatatan
Int42integer berukuran tetap (64-bit dengan GHC di platform umum)
Integer2 ^ 100integer dengan presisi arbitrer
Double3.14floating point 64-bit
BoolTrue, Falseconstructor diawali huruf kapital
Char'h'karakter Unicode
String"text"list dari Char, yaitu [Char]; Data.Text lebih cepat

show mengubah sebuah nilai menjadi String, dan print adalah putStrLn . show. Angka tidak pernah dikonversi secara implisit; gunakan fromIntegral, round, atau truncate.

Fungsi, guard, dan pattern matching

Pemanggilan fungsi ditulis dengan spasi, bukan tanda kurung: add 2 3. Setiap fungsi menerima satu argumen dan mengembalikan fungsi lain untuk sisanya, fitur yang disebut currying, sehingga add 2 sendiri adalah sebuah fungsi. Pattern matching dan guard menggantikan sebagian besar rangkaian if.

add :: Int -> Int -> Int
add x y = x + y

factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)

grade :: Int -> Char
grade score
  | score >= 90 = 'A'
  | score >= 70 = 'B'
  | otherwise   = 'C'

describe :: [a] -> String
describe []  = "empty"
describe [_] = "one item"
describe _   = "many items"

main :: IO ()
main = do
  print (add 2 3)                 -- 5
  print (map (add 10) [1, 2, 3])  -- [11,12,13] (partial application)
  print (factorial 20)            -- 2432902008176640000
  print (grade 75)                -- 'B'
  putStrLn (describe "hi")        -- many items
  print (if even 4 then "even" else "odd")  -- "even"

if adalah ekspresi dan selalu memerlukan else. Tidak ada perulangan: pengulangan dilakukan dengan rekursi atau fungsi tingkat tinggi seperti map, filter, dan foldr.

List, tuple, dan fungsi tingkat tinggi

List menyimpan nilai dengan satu tipe dan merupakan koleksi yang paling umum. Tuple mengelompokkan sejumlah nilai tetap yang tipenya boleh berbeda. List comprehension, range, dan operator komposisi fungsi . membuat pengolahan data menjadi ringkas.

import Data.List (sortOn)

main :: IO ()
main = do
  let nums = [5, 3, 8, 1] :: [Int]
  print (head nums, length nums, sum nums)       -- (5,4,17)
  print (0 : nums)                               -- [0,5,3,8,1] (prepend)
  print (filter even nums)                       -- [8]
  print (foldr (+) 0 [1 .. 10])                  -- 55
  print [x * x | x <- [1 .. 5], odd x]           -- [1,9,25]
  print (take 5 [1, 3 ..])                       -- [1,3,5,7,9] (lazy infinite list)
  let people = [("Ann", 31), ("Bo", 25)] :: [(String, Int)]
  print (map fst (sortOn snd people))            -- ["Bo","Ann"]
  print ((length . filter (> 2)) nums)           -- 3
  print (zip [1 :: Int ..] "abc")                -- [(1,'a'),(2,'b'),(3,'c')]

Karena Haskell bersifat lazy, nilai dihitung hanya saat diperlukan. Itulah sebabnya take 5 [1, 3 ..] bekerja pada list tak hingga. Kelambanan ini juga bisa menumpuk ekspresi besar yang belum dievaluasi; pilih foldl' dari Data.List daripada foldl untuk menjumlahkan list yang panjang.

Algebraic data type dan type class

Anda mendefinisikan tipe sendiri dengan data. Sebuah tipe dapat punya beberapa constructor (sum type), masing-masing dengan field (product type). Type class seperti Show, Eq, Ord, dan Functor menggambarkan perilaku bersama, mirip interface; deriving membuat instance yang umum secara otomatis.

data Shape = Circle Double | Rect Double Double
  deriving (Show, Eq)

area :: Shape -> Double
area (Circle r) = pi * r * r
area (Rect w h) = w * h

data User = User { userName :: String, userAge :: Int }
  deriving Show

class Describable a where
  describe :: a -> String

instance Describable Shape where
  describe (Circle _) = "a circle"
  describe (Rect _ _) = "a rectangle"

main :: IO ()
main = do
  print (map area [Circle 1, Rect 2 3])   -- [3.141592653589793,6.0]
  putStrLn (describe (Rect 1 1))          -- a rectangle
  let u = User { userName = "Ann", userAge = 31 }
  print (u { userAge = 32 })              -- User {userName = "Ann", userAge = 32}

Maybe, Either, dan IO

Haskell tidak punya null dan tidak punya exception tanpa pemeriksaan di kode murni. Nilai yang mungkin tidak ada bertipe Maybe a (Just x atau Nothing), dan komputasi yang bisa gagal beserta alasannya bertipe Either e a (Left err atau Right x). Efek samping seperti membaca input berada dalam tipe IO, dan notasi do mengurutkannya.

import Text.Read (readMaybe)

safeDiv :: Int -> Int -> Either String Int
safeDiv _ 0 = Left "division by zero"
safeDiv x y = Right (x `div` y)

parseAge :: String -> Maybe Int
parseAge s = case readMaybe s of
  Just n | n >= 0 -> Just n
  _               -> Nothing

main :: IO ()
main = do
  print (safeDiv 10 2)          -- Right 5
  print (safeDiv 1 0)           -- Left "division by zero"
  print (parseAge "42")         -- Just 42
  print (parseAge "abc")        -- Nothing
  print (fmap (+ 1) (Just 41))  -- Just 42
  putStrLn "Your name?"
  n <- getLine                  -- an IO action that returns a String
  putStrLn ("Hi, " ++ n)

Program lengkap kecil: frekuensi kata

Program ini membaca teks dari standard input, menghitung frekuensi kata dengan Map, dan mencetak tiga kata terbanyak. Program ini memakai komposisi fungsi, Data.Map dari paket containers yang disertakan bersama GHC, dan pengurutan.

import Data.Char (isAlpha, toLower)
import Data.List (sortBy)
import Data.Ord (comparing, Down (..))
import qualified Data.Map.Strict as Map

wordFreq :: String -> [(String, Int)]
wordFreq =
  sortBy (comparing (Down . snd) <> comparing fst)
    . Map.toList
    . Map.fromListWith (+)
    . map (\w -> (w, 1))
    . words
    . map (\c -> if isAlpha c then toLower c else ' ')

main :: IO ()
main = do
  text <- getContents
  mapM_ (\(w, n) -> putStrLn (w ++ ": " ++ show n)) (take 3 (wordFreq text))
echo "The cat and the dog. The dog sat, and the cat ran." | runghc wordfreq.hs
# the: 4
# and: 2
# cat: 2

Langkah selanjutnya

  • Halaman dokumentasi di haskell.org, yang menaut ke tutorial, buku, dan GHC User's Guide.
  • Hackage, repositori paket, dan Hoogle, mesin pencari yang menemukan fungsi berdasarkan nama atau tipe.
  • Learn You a Haskell for Great Good! dan Real World Haskell, dua buku yang bisa dibaca gratis.
  • Haskell 2010 Language Report untuk definisi bahasa yang presisi.

FAQ

Apakah Haskell sulit dipelajari?

Terasa berbeda karena tidak ada variabel mutable maupun perulangan, dan memakai sistem tipe yang ekspresif. Pemula yang mempelajari rekursi, pattern matching, dan tipe selangkah demi selangkah biasanya menemukan bahwa bahasa intinya kecil dan konsisten.

Apa arti fungsional murni?

Fungsi selalu mengembalikan hasil yang sama untuk input yang sama dan tidak mengubah state atau melakukan input dan output. Efek tetap dimungkinkan, tetapi dinyatakan dalam tipe seperti IO.

Apa itu monad di Haskell?

Monad adalah type class untuk tipe seperti Maybe, Either, dan IO yang mendukung perangkaian komputasi dengan >>=. Notasi do adalah sintaks untuk perangkaian itu.

Apa itu lazy evaluation?

Ekspresi dievaluasi hanya ketika nilainya diperlukan. Ini memungkinkan list tak hingga dan menghindari pekerjaan yang tidak perlu, tetapi dapat membuat penggunaan memori lebih sulit diprediksi.

Sebaiknya pakai Stack atau Cabal?

Keduanya membangun proyek Haskell dan keduanya dipasang oleh GHCup. Cabal adalah build tool standar; Stack menambahkan snapshot paket yang dikurasi. Keduanya cocok untuk pemula.

Semua tutorial pemrograman