Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Pythonで始めてみよう関数型プログラミング

 Pythonで始めてみよう関数型プログラミング

Satoshi Terajima

September 17, 2019
Tweet

Transcript

  1. お前誰よ Terajima Satoshi @meganehouser 所属 株式会社SQUEEZE Python Django / Django

    REST framework AngularJS / Angular Meguro.LYAHFGG主催 (すごいHaskell本を原書で読む会) 2
  2. 5

  3. 関数型プログラミングの例 F#のパイプライン演算⼦ let add x y = x + y

    // add : int -> int -> int let minus x y = x - y // minus : int -> int -> int let display = printfn "number is %d" // display : int -> unit // パイプライン演算⼦を使⽤しない通常の書き⽅ display(minus 20 (add 5 10)) // パイプライン演算⼦を使⽤した書き⽅ 10 |> add 5 |> minus 20 |> display // => "number is 5" 14
  4. Pythonコードにコンパイルするプログラミング⾔語 Coconut Programming Language Pythonと互換性があり、関数型⾔語の便利な機能を追加している # パイプライン演算⼦, 部分適⽤ range(10) |>

    map$(pow$(?, 2)) |> list # 代数的データ型 data Empty() data Leaf(n) data Node(l, r) # データ型によって適⽤される関数を切り替える機能 def size(Empty()) = 0 addpattern def size(Leaf(n)) = 1 addpattern def size(Node(l, r)) = size(l) + size(r) 出典: http://coconut-lang.org/ 20
  5. PythonのASTにコンパイルするプログラミング⾔語 hylang/hy: A dialect of Lisp that's embedded in Python

    ⽂法はLisp マクロもある defn simple-conversation [] (print "Hello! I'd like to get to know you. Tell me about yourself!") (setv name (input "What is your name? ")) (setv age (input "What is your age? ")) (print (+ "Hello " name "! I see you are " age " years old."))) (simple-conversation) 出典: Tutorial — hy 0.17.0 documentation 21
  6. CPython bytecodeにコンパイルするプログラミング⾔語 dg — it's a Python! No, it's a

    Haskell! Haskellのような⾒た⽬の動的⾔語 Slow. Stupid. Absolutely adorable. import '/asyncio' main = async $ loop -> task = "Hello, {}!".format whatever where await asyncio.sleep 1 whatever = "World" await task loop = asyncio.get_event_loop! loop.run_until_complete $ main loop 出典: https://pyos.github.io/dg/ 22
  7. 関数を組み合わせて… 例題:複数の割引関数を適⽤した後の価格が⾼額か少額か判定したい apple = ('apple', 300) def discount_by_day(fruit): # 曜⽇によって割引

    price = fruit[1] * 0.9 if datetime.now().weekday() in (5, 6) else fruit[1] return fruit[0], price def discount_by_time(fruit): # 時間帯によって割引 price = fruit[1] * 0.9 if datetime.now().hour > 21 else fruit[1] return fruit[0], price def get_price_rank(fruit): # 価格によって価格区分を返す return 'High'if fruit[1] >= 300 else 'Low' 28
  8. [解決に使うFP機能] 関数合成 2個の関数を合成して新しい1個の関数を作り出す機能 専⽤の演算⼦を使⽤して関数合成を⾏う 処理する順序で関数を並べて定義できるので可読性が⾼い F# での関数合成の例 let add50 x

    = x + 50 // add50 : int -> int let add100 x = x + 100 // add100 : int -> int let minus100 x = x - 100 // minus100 : int -> int // newFunc : int -> int let newFunc = add50 >> add100 >> minus100 newFunc 10 // => 10 30
  9. Pythonで関数合成を提供するパッケージ fn.py fnpy/fn.py: Missing features of fp in Python --

    active fork of kachayev/fn.py pip3 install fn.py Scalaスタイルのラムダ式 永続データ構造 ストリームと無限シーケンス 関数のカリー化 etc. 32
  10. fn.Fの関数合成の実装⽅法 Pythonでは演算⼦は特殊メソッドのオーバーロードによって実現 新たな記号を使⽤した演算⼦を増やすことはできない Fクラスの__rshift__をオーバーロードして >> 演算⼦の機能を上書 きしている class F(object): @classmethod

    def __compose(cls, f, g): return cls(lambda *args, **kwargs: f(g(*args, **kwargs))) def __ensure_callable(self, f): return self.__class__(*f) if isinstance(f, tuple) else f def __rshift__(self, g): return self.__class__.__compose(self.__ensure_callable(g), self.f) ソース出典: fn.py/func.py at master · fnpy/fn.py 34
  11. 関数の引数が増えたら… 例題: rateを引数で指定できるようにする def discount_by_day(rate, fruit): price = fruit[1] *

    rate if datetime.now().weekday() in (5,6) else fruit[1] return fruit[0], price def discount_by_time(rate, fruit): price = fruit[1] * rate if datetime.now().hour >= 21 else fruit[1] return fruit[0], price def get_price_rank:(fruit): return 'High'if fruit[1] > 300 else 'Low' 37
  12. カリー化 カリー化 (currying, カリー化された=curried) とは、複数の引数を とる関数を、引数が「もとの関数の最初の引数」で戻り値が「もと の関数の残りの引数を取り結果を返す関数」であるような関数にす ること(あるいはその関数のこと)である。 [出典] カリー化

    - Wikipedia // 3 引数の関数は // (int, int, int) -> int だと普通は考えるが... let add x y z = x + y + z // add: int -> int -> int -> int let add' = add 1 // add': (1) int -> int -> int let add'' = add' 2 // add'': (1) (2) int -> int let result = add'' 3 // result: (1) (2) (3) int // => 6 40
  13. Pythonで関数をカリー化を提供するパッケージ fn.pyのcurriedデコレータで関数をカリー化できる >>> from fn.func import curried >>> @curried ...

    def sum5(a, b, c, d, e): ... return a + b + c + d + e ... >>> sum5(1)(2)(3)(4)(5) 15 >>> sum5(1, 2, 3)(4, 5) 15 出典: https://github.com/fnpy/fn.py#function-currying 41
  14. 標準モジュールのfunctools.partial 関数に引数の部分適⽤したCallableオブジェクトを返す >>> from functools import partial >>> basetwo =

    partial(int, base=2) >>> basetwo.__doc__ = 'Convert base 2 string to an int.' >>> basetwo('10010') 18 出典: functools --- ⾼階関数と呼び出し可能オブジェクトの操作 — Python 3.7.4 ドキュメント 45
  15. listを操作する関数の問題点 例題: 関数でlistを操作して結果を⽐較する def add_mango(fs: List[str]) -> List[str]: fs.append('mango') return

    fs def change_from_apple_to_banana(fs: List[str]) -> List[str]: fs[fs.index('apple')] = 'banana' return fs fruits = ['apple', 'melon'] fruits1 = add_mango(fruits) fruits2 = change_from_apple_to_banana(fruits) assert fruits1 != fruits2 # AssertionError !! Pythonの関数は参照渡しのため引数で渡したリストも変更される 関数呼び出しごとにリストをコピーする必要がある 毎回listをコピーする必要があるので⾮効率 50
  16. Pythonで不変・永続データ構造を提供するパッケージ tobgu/pyrsistent Python標準モジュールのlist,tuple,dict,classなどに似せた不変/永 続/関数型のデータ型を提供するパッケージ >>> from pyrsistent import v, pvector

    >>> v1 = v(1, 2, 3) >>> v2 = v1.append(4) >>> v3 = v2.set(1, 5) >>> v1 pvector([1, 2, 3]) >>> v2 pvector([1, 2, 3, 4]) >>> v3 pvector([1, 5, 3, 4]) 出典: https://pyrsistent.readthedocs.io/en/latest/intro.html#pvector 53
  17. [解決] 永続データ構造に置き換える 例題のリスト操作関数を永続データ構造に置き換えたコード from pyrsistent import v, PVector def add_mango(fs:

    PVector[str]) -> PVector[str]: return fs.append('mango') def change_from_apple_to_banana(fs: PVector[str]) -> PVector[str]: return fs.set(fs.index('apple'), 'banana') fruits = ['apple', 'melon'] fruits1 = add_mango(fruits) fruits2 = change_from_apple_to_banana(fruits) assert fruits1 != fruits2 引数に渡したlistが変更されないことで純粋関数となり使いやすくなった 54
  18. dictのkeyの有無の判定で条件分岐する場合の問題点 def reserve(request): num_of_children = request['numbers'].get('children') date_ = request['date'] num_of_adults

    = request['numbers']['adults'] if num_of_children is None: return reserve_general_room(date, num_of_adults) else: return reserve_familly_room(date, num_of_adults, num_of_children) 関数の実装から、想定されているdictの形式が掴みづらい 60
  19. [解決に使⽤するFP機能] パターンマッチ 構造を持つデータを分解し、構成要素を取り出す 構造または分解・取得したデータによって条件分岐を⾏う F#でのパターンマッチの例 type Person = {Name: string;

    Gender: int} // 1: male, 2: female let genderName person = match person with | { Name=n; Gender=1 } -> n + " is male" | { Name=n; Gender=2 } -> n + " is female" | _ -> "undeterminded" genderName {Name="Taro"; Gender=1} 61
  20. Pythonでパターンマッチを提供するパッケージ santinic/pampy: Pampy: The Pattern Matching for Python you always

    dreamed of. Pythonでは制御構⽂は追加できないので、引数で callback関数を指定する ⽂字列, 数値, tuple, list, dict, dataclass等、様々なデー タ形式でマッチングが可能 from pampy import match, _ input = [1, 2, 3] pattern = [1, 2, _] action = lambda x: f"it's {x}" match(input, pattern, action) 出典: https://github.com/santinic/pampy 62
  21. Pampyで複数のパターンを使う場合 match([1,2,3], [1, 2, _], lambda x: f"it's {x}", [9,

    9, 9], lambda x: "All nine", _, lambda x: "unmatch", ) pattern, actionのpairを複数書く _ 単体のpatternはどんな値にもmatchするため、デフォルトの actionの定義できる 63
  22. [解決] 条件分岐をパターンマッチで書き換える 例題の条件分岐をパターンマッチで書き換えたコード from pampy import match, _ def reserve(request):

    return match(request, {'date': _, 'numbers': {'adults': _, 'children': _}}, reserve_familly_room, {'date': _, 'numbers': {'adults': _}}, reserve_general_room, ) 想定されるデータ形式が把握しやすい 想定されるデータ形式と、アクションが連続しているので可読性が ⾼い 64
  23. 70

  24. Functor Functorの定義 class Functor f where fmap :: (a ->

    b) -> f a -> f b Functor則 Functorを実装する際に満たすべき規則 # id でファンクター値を写した場合、ファンクター値が変化してはいけない fmap id = id # 、すべてのファンクター値x に対して以下の等式が成り⽴つ fmap (f . g) x = fmap f (fmap g x) 72
  25. Applicative Functor Applicative Functorの定義 class Functor f => Applicative f

    where pure :: a -> f a (<*>) :: f (a -> b) -> f a -> f b 73
  26. Monad Monadの定義 class Applicative m => Monad m where return

    :: a -> m a (>>=) :: m a -> (a -> m b) -> m b (>>) :: m a -> m b -> m b x >> y = x >>= _ -> y fail :: String -> m a fail :: msg = error msg 74
  27. モナド則 モナドを実装するときに満たすべき規則 左恒等性 return x >>= f == f x

    右恒等性 m >>= return == m 結合性 (m >>= f) >>= g == m >>= (\x -> f x >>= g) 75
  28. モナドの例 1 「Maybeモナド」 Maybeは値が存在する|存在しない⽂脈を持った型 data Maybe a = Just a

    | Nothing 例としてx + yが100を超える場合はNothingを返す関数を定義 addLimit :: Int -> Int -> Maybe Int addLimit x y | (x + y) <= 100 = Just(x + y) | otherwise = Nothing 78
  29. Maybeモナドのバインド関数 バインド関数 (>>=) 左辺の⽂脈付きの値を、右辺の⽂脈なし引数を取り⽂脈付き値を返す関 数に適⽤する演算⼦ Just 10 >>= addLimit 20

    >>= addLimit 30 -- Just 60 Just 20 >>= addLimit 90 >>= addLimit 60 -- Nothing 右辺値がNothingの場合は関数をバイパスしてNothingを返すよう に定義されている 79
  30. 戻り値が存在しない場合がある関数の組み合わせ 例題: 存在しない可能性のある値を取得する関数の組み合わせ def get_user(user_id: int) -> Optional[User]: try: return

    User.objects.get(pk=user_id) except DoesNotExist: return None def get_user_photo(user: User) -> Optional[UserPhoto]: try: return UserPhoto.objects.get(user=User) except DoesNotExist: return None def get_photo_datetime(user_photo: UserPhoto) -> datetime: return user_photo.datetime 81
  31. 普通に組み合わせた場合の問点 user_id = 101 user = get_user(user_id) if user: user_photo

    = get_user_photo(user) if user_photo: return get_photo_datetime(user_photo) if⽂ごとにネストが深くなってしまう 82
  32. [解決] Maybeモナド まずは関数を、⽂脈なしの引数をとって⽂脈ありの戻り値を返す関数に 書き直す from monads.maybe import Maybe, Just, Nothing

    def get_user(user_id: int) -> Maybe[User]: try: return Just(User.objects.get(pk=user_id)) except DoesNotExist: return Nothing() def get_user_photo(user: User) -> Maybe[UserPhoto]: try: return Just(UserPhoto.objects.get(user=User)) except DoesNotExist: return Nothing() def get_photo_datetime(user_photo: UserPhoto) -> Maybe[datetime]: return Just(user_photo.datetime) 83
  33. [解決] Maybeモナド 次に関数をbind関数(bind演算⼦)で結合する Just(101) >> get_user >> get_user_photo >> get_photo_datetime

    Pythonでは新たな記号を⽤いた演算⼦を作成することはできない typesafe-monadではbind関数を >> に割り当てている Pythonは新たな演算⼦は増やせないのでパッケージ間で使⽤ する記号がかぶりがち 84