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:
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