Programuj bezpiecznie, typie

Typy i programowanie funkcyjne

Po co nam typy?

digits = datasets.load_digits()
n_samples = len(digits.images)
X = digits.images.reshape((n_samples, -1))
y = digits.target

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.5, random_state=0)

tuned_parameters = [{'kernel': ['rbf'], 'gamma': [1e-3, 1e-4],
                     'C': [1, 10, 100, 1000]},
                    {'kernel': ['linear'], 'C': [1, 10, 100, 1000]}]

scores = ['precision', 'recall']

for score in scores:
    clf = GridSearchCV(SVC(), tuned_parameters, cv=5, scoring='%s_macro' % score)
    clf.fit(X_train, y_train)

    print("Best parameters set found on development set:", clf.best_params_)
    means = clf.cv_results_['mean_test_score']
    stds = clf.cv_results_['std_test_score'],
    for mean, std, params in zip(means, stds, clf.cv_results_['params']):
        print("%0.3f (+/-%0.03f) for %r" % (mean, std * 2, params))
    y_true, y_pred = y_test, clf.predict(X_test)
    print(classification_report(y_true, y_pred))

Po co nam typy?

Po co nam typy?

The simplified version of Parity Wallet Hack 1 is as follows:

The Wallet contract is a simple contract that uses delegatecall to execute transactions using WalletLibrary ’s code within the context of the Wallet function. The WalletLibrary contract assumes that it will be called in the context of a contract that does have state that it can modify (namely m_numOwners) and this is the core assumption that caused the recent disaster. In the first Parity Wallet Hack, the hacker changed the state of various Wallet contracts by delegating a call to initWallet, setting themselves as the owner of the Wallet contract and then withdrawing funds normally.

Po co nam typy?

154 miliony dolarów...

The WalletLibrary contract assumes that it will be called in the context of a contract that does have state that it can modify (namely m_numOwners) and this is the core assumption that caused the recent disaster.

154 miliony dolarów...

Po co nam typy?

Pozwalają nam:

  • znajdować błędy podczas kompilacji
  • część testów pisze się sama
  • weryfikować poprawność kluczowego kodu

Krótki kurs Haskella

Funkcje

Definicja:

foo :: Int -> Int
foo x = x + 10

bar :: String -> Int
bar s = length s

Wywołanie:

foo 42

bar "Haskell is love, haskell is life"

Typy danych

Definicja:

data Foo = Foo Int | Bar String

Stworzenie wartości:

foo = Foo 42

bar = Bar "Haskell"

Użycie w funkcji:

someFun :: Foo -> Foo
someFun (Foo x) = Foo (x + 1)
someFun (Bar s) = Bar (s ++ s)

Newtype

Definicja:

newtype Dollar = Dollar Double

Stworzenie wartości:

amountUsd = Dollar 123.0

Użycie w funkcji:

someFun :: Dollar -> Dollar
someFun (Dollar amt) = Dollar $ amt + 10.0

Let/where

Let:

let x = 15 in x + 10

foo :: Int -> Foo
foo x =
    let s = show (x + 100)
    in  Bar s

Where:

x + 10 where x = 15

foo :: Int -> Foo
foo x = Bar s
    where s = show (x + 100)

Wyrażenia Lambda

Funkcje bez nazwy:

\x -> x + 1

\x y -> x + y

Użycie:

filter (\x -> x `mod` 2 == 1) [1,2,3,4,5]

map (\x -> x + 10) [1,2,3,4,5]   -- map (+ 10) [1,2,3,4,5]

Polimorfizm

Prawdziwy, parametryczny!

foo :: a -> [a]
foo x = [x, x, x]

map :: (a -> b) -> [a] -> [b]
map f []     = []
map f (x:xs) = f x : map f xs

(.) :: (b -> c) -> (a -> b) -> a -> c
(.) f g x = f (g x)

Typy danych:

data Foo a b = Foo a | Bar b

Funkcje:

Przydatne sztuczki

Dolar (aplikacja funkcji):

f (g x)

-- to to samo, co:

f $ g x

Bezpunktowość:

f x = g x

-- to to samo, co

f = g

-- natomiast:

foo x = f $ g x

-- to to samo, co:

foo = f . g

Typeclassy

Definicja:

class Addable a where
    add  :: a -> a -> a
    zero :: a

Instancja:

reduce :: Addable a => [a] -> a
reduce = foldl add zero

Wykorzystanie w funkcji:

instance Addable Foo where
    add (Foo x) (Foo y) = Foo $ x + y
    add (Foo x) (Bar s) = Bar $ show x <> s
    add (Bar s) (Bar z) = Bar $ s <> z

    zero = Foo 0

Rodziny typów

Ulepszmy nieco naszą typeclassę!

class Addable a b where
    type Res a b
    add  :: a -> b -> Res a b

Instancja:

data Foo = Foo Int
data Bar = Bar String

instance Addable Foo Bar where
    type Res Foo Bar = Bar
    add (Foo x) (Bar s) = Bar $ show x <> s
   
instance Addable Bar Foo where
    type Res Bar Foo = Bar
    add f b = add b f

instance Addable Foo Foo where
    type Res Foo Foo = Foo
    add (Foo x) (Foo y) = Foo $ x + y
    
instance Addable Bar Bar where
    type Res Bar Bar = Bar
    add (Bar s) (Bar z) = Bar $ s <> z

Monady!!!

Potrzebny tylko jeden tutorial:

Philip Wadler, Monads for functional programming

W dużym skrócie:

someFunc :: Int -> IO Int
someFunc x = do
    let y = x + 10
    line <- getLine
    pure $ y + length line


f :: Int -> Int

f <$> someFunc 10

g :: IO Int

g >>= someFunc

h :: Int -> IO Int

h >=> someFunc

Polimorfizm wyższego rzędu

Kojarzycie identyczność?

id :: forall a. a -> a
id a = a

Taką funkcję można przekazać jako parametr:

poly id.            -- działa!

poly (\n -> n + 1)  -- nie działa! za mało polimorficzna!

I wtedy musimy podać prawdziwie polimorficzną funkcję:

poly :: (forall a. a -> a) -> Bool
poly f = (f 0 < 1) == f true

Sztuczki 2.0

ptr :: Ptr Int <- malloc

Da się ładniej?

sizeOf :: Storable a => a -> Int

sizeOf (undefined :: Int) --- bardzo brzydko!

Przyjrzyjmy się bliżej. Weźmy sizeOf:

ptr <- malloc @Int

Zaalokujmy sobie pamięć:

sizeOf' :: forall a. Storable a => a -> Int
sizeOf' = sizeOf (undefined :: @a)

sizeOf' @Int    -- dużo lepiej!

Naprawmy to.

Bezpieczne programowanie

There's no such thing as a strongly typed language

M. Snoyman

Napiszmy sobie... BANK

data BankState = BankState
    { transactions    :: [(String, String, Double)]
    , conversionRates :: Map (String, String) Double
    } deriving Show


wireMoney :: String -> String -> Double -> String -> BankState -> BankState
wireMoney fromAddr toAddr amount currency oldState = newState
    where transaction      = (fromAddr, toAddr, amountUSD)
          transactions'    = transaction : transactions oldState
          conversionRates' = conversionRates oldState
          amountUSD        = convert currency "USD" amount oldState
          newState         = BankState transactions' conversionRates'

Problem 1: nie o takiego Haskella walczyłem.

Rozwiązanie: użyjmy Lensów.

BANK, podejście nr 2

data BankState = BankState
    { _transactions    :: [(String, String, Double)]
    , _conversionRates :: Map (String, String) Double
    } deriving Show
makeLenses ''BankState


wireMoney :: String -> String -> Double -> String -> BankState -> BankState
wireMoney fromAddr toAddr amount currency oldState =
    oldState & transactions %~ (transaction :)
    where transaction = (fromAddr, toAddr, amountUSD)
          amountUSD   = convert currency "USD" amount oldState

Problem 2: to nie jest type safe.

Rozwiązanie: użyjmy newtype'ów!

BANK, podejście nr 3

newtype FromAddr = FromAddr String deriving Show
newtype ToAddr   = ToAddr   String deriving Show

data BankState = BankState
    { _transactions    :: [(FromAddr, ToAddr, Double)]
    , _conversionRates :: Map (String, String) Double
    } deriving Show
makeLenses ''BankState

wireMoney :: FromAddr -> ToAddr -> Double -> String -> BankState -> BankState
wireMoney fromAddr toAddr amount currency oldState =
    oldState & transactions %~ (transaction :)
    where transaction = (fromAddr, toAddr, amountUSD)
          amountUSD   = convert currency "USD" amount oldState

Problem 3: konwersja walut dalej nie jest type safe.

Rozwiązanie: użyjmy typeclass!

Lepiej! Nie pomylimy nadawcy z odbiorcą.

Konwersja walut do tej pory

convert :: String -> String -> Double -> BankState -> Double
convert fromCurr toCurr amount bankState = rate * amount
    where rates = bankState ^. conversionRates
          rate  = rates ! (fromCurr, toCurr)

NAJ-GO-RZEJ.

 

Co się dzieje, gdy danej pary walut nie ma w mapie?

Konwersja walut, podejście nr 2

newtype Dollar = Dollar Double deriving Show
newtype Zloty  = Zloty  Double deriving Show


class Convertible a b | a -> b where
    convert :: a -> b


instance Convertible Dollar Zloty where
    convert (Dollar d) = Zloty $ d * 3.64

instance Convertible Zloty Dollar where
    convert (Zloty z) = Dollar $ z * 0.27

Teraz konwersje są definiowane statycznie, nie będzie błędu w runtime'ie.

BANK, podejście nr 4

data BankState = BankState
    { _transactions    :: [(FromAddr, ToAddr, Dollar)]
    } deriving Show
makeLenses ''BankState


wireMoney :: Convertible a Dollar
          => FromAddr -> ToAddr -> a -> BankState -> BankState
wireMoney fromAddr toAddr amount = transactions %~ (transaction :)
    where transaction = (fromAddr, toAddr, amountUSD)
          amountUSD   = convert amount

I tutaj już wygląda to dużo lepiej.

 

Jeszcze kosmetyka.

BANK, podejście nr 5

data Transaction a = Transaction
    { _fromAddr :: FromAddr
    , _toAddr   :: ToAddr
    , _amount   :: a
    } deriving Show
makeLenses ''Transaction

newtype Ledger = Ledger
    { _transactions :: [Transaction Dollar]
    } deriving Show
makeLenses ''Ledger

type BankState a = StateT Ledger IO a

emptyState :: Ledger
emptyState = Ledger []


wireMoney :: (Convertible a Dollar, MonadState Ledger m, MonadIO m)
          => Transaction a -> m ()
wireMoney t = do
    let t' = t & amount %~ convert
    State.modify $ transactions %~ (t' :)

runBank :: BankState () -> IO Ledger
runBank = flip State.execStateT emptyState

BANK, podejście nr 5

Bank5.runBank $ do
    let from  = Bank5.FromAddr "from"
        to    = Bank5.ToAddr   "to"
        trans = Bank5.Transaction from to (Bank5.Zloty 123.0)

    Bank5.wireMoney trans

Ładnie?

Podsumowanie

  • Błędy podczas kompilacji są okej
  • Błędy podczas wykonania: nie
  • Nie należy używać częściowych funkcji (Prelude.head, Map.!)
  • Jeśli coś da się rozróżnić na poziomie typów, to warto
  • Należy testować kod
  • Należy pisać funkcje, które da się testować
  • W Haskellu da się napisać program, który jest beznadziejny

Przydatne linki

  • https://www.fpcomplete.com/blog/2018/01/weakly-typed-haskell
  • http://www.vlachjosef.com/tagged-types-introduction/
  • https://hackernoon.com/parity-wallet-hack-2-electric-boogaloo-e493f2365303
  • https://wiki.haskell.org/Typeclassopedia
  • https://typeclassopedia.bitbucket.io/
  • https://people.mpi-sws.org/~dreyer/tor/papers/wadler.pdf
  • http://hspec.github.io/
  • https://youtu.be/IOiZatlZtGU

Type safe programming

By Piotr Moczurad

Type safe programming

  • 454