Moduly a balíky

Ciele

  1. Naučiť sa používať Cabal na definovanie projektov.
  2. Naučiť sa použivať moduly.
  3. Naučiť sa používať externé knižnice.
  4. Naučiť sa pracovať s realistickými údajovými štruktúrami.

Úvod

Na definovanie a kompiláciu komplexného programu alebo knižnice sa používa nástroj cabal-install. Je to nástroj pre zostavenie a zároveň správca závislostí.

Postup

Krok 1: Inicializácia projektu

Úloha 1.1

Aktualizujte databázu balíkov pre Haskell pomocou príkazu cabal update.

Úloha 1.2

Vytvorte nový adresár s názvom tuke-minesweeper a so základnými súbormi projektu.

mkdir tuke-minesweeper
cd tuke-minesweeper

V tomto adresári tuke-minesweeper vytvorte tri súbory. Prvý z nich je tuke-minesweeper.cabal, ktorý definuje vlastnosti projektu:

cabal-version:      >=1.10

name:               tuke-minesweeper
version:            0.1.0.0
synopsis:           Simple console minesweeper game
license:            MIT
author:             Sergej Chodarev
build-type:         Simple

executable minesweeper
  main-is:          Main.hs
  build-depends:    base >=4.13 && <5
  default-language: Haskell2010
  ghc-options:      -Wall

Ďalší súbor je Setup.hs, ktorý v našom prípade obsahuje iba predvolenú logiku:

import Distribution.Simple
main = defaultMain

Tretím súborom je kód našej aplikácie — Main.hs:

module Main where

main :: IO ()
main = putStrLn "Hello, Haskell!"

Poznámka

Pre vytvorenie projektu je možné použiť aj príkaz cabal init. Tento príkaz vygeneruje kostru projektu automaticky.

Úloha 1.3

Preložte a spustite program. Na preloženie programu slúži príkaz cabal build a na jeho spustenie cabal run minesweeper.

Poznámka

Väčšinou stačí použiť iba run, ktorý automaticky preloži program, ak boli zmenené zdrojové kódy.

Krok 2: Doplnenie modulu a závislostí

Úloha 2.1

Pridajte do súboru tuke-minesweeper.cabal ďalší modul s názvom Minesweeper.Board a doplňte závislosti podľa vzoru:

executable minesweeper
  main-is:          Main.hs
  other-modules:    Minesweeper.Board
  build-depends:    base >=4.13 && <5,
                    containers ==0.6.*
  default-language: Haskell2010

Týmto sme definovali, že okrem modulu Main sa kód nášho programu bude nachádzať v jednom ďalšom module. Zároveň sme zväčšili rozsah podporovaných verzií štandardnej knižnice Haskellu a doplnili knižnicu containers.

Krok 3: Moduly

Úloha 3.1

Vytvorte súbor Minesweeper/Board.hs, ktorý bude obsahovať definíciou hracieho poľa hry Míny.

Kód modulu musí obsahovať hlavičku definujúcu jeho názov a zoznam exportovaných symbolov:

module Minesweeper.Board
    (
    -- * Board
    -- ** Types
      Config(..)
    , Board
    , Pos(..)
    , Tile(..)
    -- ** Board creation
    , createBoard
    , smallBoard
    ) where

Zápis Config(..) znamená, že exportovaný je dátový typ spolu so všetkými jeho konštruktormi. Na rozdiel od toho, typ Board neexportuje konštruktory, takže v iných moduloch nie je možné ani vytvárať jeho hodnoty, ani ich rozkladať pomocou porovnávania vzorov. Prístup k jeho hodnotam je teda možný iba pomocou poskytnutých funkcií, čo zabezpečí skrytie jeho implementácie.

Za hlavičkou modulu nasledujú importy iných modulov:

import Data.Set (Set, member, notMember)  -- most used set operations
import qualified Data.Set as Set          -- the rest of operations

Následne máme definície dátových typov:

-- Board definition ---------------------------------------------------------

-- | Configuration of the board.
data Config = Config
    { configSize :: Pos      -- ^ board size
    , configNumMines :: Int  -- ^ number of mines
    } deriving (Eq, Show)

{- | Complete state of the board. It includes the configuration and hidden
properties.

The structure of the board is not exported. To create the board, use
'createBoard' smart constructor.
 -}
data Board = Board
    { bConfig :: Config
    , bMines :: Set Pos
    , bOpened :: Set Pos
    , bMarked :: Set Pos
    } deriving (Show)  -- only for testing


-- | Position of the tile or size of the board.
data Pos = Pos
    { posRow :: Int
    , posCol :: Int
    } deriving (Eq, Ord, Show)

-- | Tile state.
data Tile
    = Open Int -- ^ Open tile with number of neighbour mines
    | Closed   -- ^ Closed tile
    | Marked   -- ^ Tile marked for mine
    | Exploded -- ^ Exploded (opened) mine
    deriving (Eq, Show)

-- Board creation -----------------------------------------------------------

-- | Create a new board with random mines.
createBoard :: Config -> [Pos] -> Board
createBoard config positions = Board
    { bConfig  = config
    , bMines   = Set.fromList positions
    , bOpened  = Set.empty
    , bMarked  = Set.empty
    }

-- | Configuration for a small board.
smallBoard :: Config
smallBoard = Config (Pos 8 8) 10

Úloha 3.2

Upravte modul Main, tak aby ste si vyskúšali prácu s hracím poľom.

module Main where

import Minesweeper.Board

main :: IO ()
main = putStrLn (show exampleBoard)
  where
    exampleBoard = createBoard smallBoard [Pos x y | x <-[1, 3 .. 10], y <- [2, 4]]

Krok 4: Doplnenie funkcií

Úloha 4.1

Doplňte deklaráciu ďalších exportovaných funkcií v module Minesweeper.Board:

    -- ** Properties
    , boardSize
    , boardNumMines
    , boardNumMarked
    , validPos
    , tileState
    , isExploded
    , isOpen
    , isMarked
    , isClosed
    , neighbours

Úloha 4.2

Doplňte implementáciu chýbajúcich funkcií.

Môžete použiť príklady definícií hodnôt a typové definície:

-- | Size of the board.
boardSize :: Board -> Pos
boardSize = configSize . bConfig

-- | Number of mines on the board.
boardNumMines :: Board -> Int
boardNumMines = configNumMines . bConfig

-- | Number of marked tiles on the board.
boardNumMarked :: Board -> Int
boardNumMarked = Set.size . bMarked

-- | Check if the position is within bounds of the board.
validPos :: Board -> Pos -> Bool
validPos board (Pos row col) =
    row >= 1 && row <= maxRow && col >= 1 && col <= maxCol
  where
    (Pos maxRow maxCol) = boardSize board

-- | Get state of the tile at specific position.
tileState :: Board -> Pos -> Tile
tileState board pos
    | isExploded board pos = Exploded
    | isOpen board pos     = Open (clue board pos)
    | isMarked board pos   = Marked
    | otherwise            = Closed

-- | Is tile on the position an opened mine?
isExploded :: Board -> Pos -> Bool
isExploded board pos = pos `member` bOpened board && pos `member` bMines board

-- | Is tile on the position open?
isOpen :: Board -> Pos -> Bool
isOpen board pos = pos `member` bOpened board && not (pos `member` bMines board)

-- | Is tile on the position marked?
isMarked :: Board -> Pos -> Bool
isMarked board pos = pos `member` bMarked board

-- | Is tile on the position closed?
isClosed :: Board -> Pos -> Bool
isClosed board pos = undefined

-- | Get positions of neighbour tiles of specified tile.
neighbours :: Board -> Pos -> [Pos]
neighbours = undefined

clue :: Board -> Pos -> Int
clue = undefined

Zdroje

  1. A Gentle Introduction to Haskell: Modules
  2. Dokumentácia nástroja Cabal