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

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

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.

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

Avatar for Satoshi Terajima

Satoshi Terajima

September 17, 2019
Tweet

Other Decks in Programming

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