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

iOSアプリ開発で 関数型プログラミングを実現する The Composable Archit...

Avatar for yimajo yimajo
June 15, 2025

iOSアプリ開発で 関数型プログラミングを実現する The Composable Architectureの紹介

Avatar for yimajo

yimajo

June 15, 2025
Tweet

More Decks by yimajo

Other Decks in Programming

Transcript

  1. ͜ͷൃදͰ͸ "QQMFϓϥοτϑΥʔϜ  J04 NBD04 UW04 XBDI04 WJTJPO04 ༻ ΞϓϦ։ൃͷͨΊͷ044ϑϨʔϜϫʔΫ

    5IF$PNQPTBCMF"SDIJUFDUVSFʢ5$"ʣΛར༻ͨ͠ ؔ਺ܕϓϩάϥϛϯάͬΆ͍՝୊ղܾʹ͍ͭͯ આ໌ͯ͠Έ·͢ɻ
  2. ؔ਺ʹର͢Δ෭࡞༻ͷఆٛ w ม਺Λมߋ w σʔλߏ଄Λ௚઀มߋ͢Δ w ΦϒδΣΫτͷϑΟʔϧυΛઃఆ w ྫ֎Λεϩʔɺ·ͨ͸Τϥʔఀࢭ w

    ίϯιʔϧʹग़ྗ͢Δɺ·ͨ͸ϢʔβʔೖྗΛಡΈऔΔ w ϑΝΠϧΛಡΈऔΔɺ·ͨ͸ϑΝΠϧʹॻ͖ࠐΉ w ը໘্ʹඳը͢Δ Ҿ༻: ʮScalaؔ਺ܕσβΠϯ&ϓϩάϥϛϯάʯ ʮiOSΞϓϦ։ൃͰ͸΄ͱΜͲ෭࡞༻·ΈΕ͔͠ͳ͍…ɻͲ͏͢Μͷʁʯ 🐲 ෭࡞༻༩͑Δ ෭࡞༻ड͚Δ
  3. 7JFX͸4UBUFͱ"DUJPOͷ3FEVDFSʹΑͬͯߏ੒ 4UBUF͕มߋ͞ΕͨΒ ϨϯμϦϯά @Reducer struct Counter { @ObservableState struct State:

    Equatable { var count = 0 } enum Action { case decrementButtonTapped case incrementButtonTapped } var body: some Reducer<State, Action> { Reduce { state, action in switch action { case .decrementButtonTapped: state.count -= 1 return .none case .incrementButtonTapped: state.count += 1 return .none } } } } struct CounterView: View { let store: StoreOf<Counter> var body: some View { HStack { Button { store.send(.decrementButtonTapped) } label: { Image(systemName: "minus") } Text("\(store.count)") .monospacedDigit() Button { store.send(.incrementButtonTapped) } label: { Image(systemName: "plus") } } } }
  4. @Reducer struct EffectsBasics { @ObservableState struct State: Equatable { var

    count = 0; var numberFact: String? var isNumberFactRequestInFlight = false } enum Action { . . . case numberFactButtonTapped case numberFactResponse(Result<String, any Error>) } @Dependency(\.factClient) var factClient var body: some Reducer<State, Action> { Reduce { state, action in switch action { . . . case .numberFactButtonTapped: state.isNumberFactRequestInFlight = true state.numberFact = nil return Effect.run { [count = state.count] send in await send(.numberFactResponse( Result { try await self.factClient.fetch(count) }) ) } case let .numberFactResponse(.success(response)): state.isNumberFactRequestInFlight = false state.numberFact = response return .none case .numberFactResponse(.failure): state.isNumberFactRequestInFlight = false return .none } } } } ෭࡞༻࣮ߦ͕ඞཁͳྫ ࣗ෼ࣗ਎΁ͷ"DUJPOOVNCFS'BDU3FTQPOTF ໭Γ஋ͱͯ͠& ff FDUΫϩʔδϟΛఆٛ ໭Γ஋ͱͯ͠& ff FDU͕ͳ͍৔߹͸& ff FDUOPOF struct EffectsBasicsView: View { let store: StoreOf<EffectsBasics> var body: some View { Section { HStack { . . . Text("\(store.count)") . . . } Button("Number fact") { store.send(.numberFactButtonTapped) } .frame(maxWidth: .infinity) . . . if let numberFact = store.numberFact { Text(numberFact) } } } }
  5. ؔ਺͔Β෭࡞༻Λ෼཭͢Δྫ ˏMainActor func x2NumberFact(_ number: Int, apiClient: APIClient) async ->

    String { let x2 = number * 2 // ७ਮͳϩδοΫ let data = await apiClient.fact(x2) return data } ؔ਺಺Ͱ෭࡞༻͕ൃੜ // ↓ // EffectͰ෼཭ͭͭ͠ɺ࣍ͷActionΛࢦఆ͢Δܗ @MainActor func x2NumberFact(_ number: Int, apiClient: APIClient) -> Effect { let x2 = number * 2 // ७ਮͳϩδοΫ return Effect.run { let data = await apiClient.fact(x2) return await send(.response(data)) } } & ff FDUSVOͷΫϩʔδϟͰ෭࡞༻͸ൃੜ͢Δ͕ɺ ͦΕ͸Y/VNCFS'BDUؔ਺ࣗମͰ෭࡞༻࣮ߦͰ͸ͳ͍Α͏ʹɺ & ff FDUSVOݺͼग़͠ݩ͕෭࡞༻Λ࣮ߦ͢ΔΑ͏ʹͳΔ w ෭࡞༻ͱͯ͠໌֬Խ͢ΔϝϦοτ w ७ਮͳϩδοΫʢ͜͜Ͱ͸YʣΛ෭࡞༻ͱ෼཭Ͱ͖Δ w டং w ίʔυͷಡΈ΍͢͞ w ͞Βʹ5$"ͷ& ff FDU͸ w 4XJGU$PODVSSFODZͰಈ࡞͢Δ w ༏ઌॱҐͷઃఆ w ฒྻ࣮ߦͷߏ଄Խॲཧ w Ωϟϯηϧॲཧ͕༻ҙ w & ff FDUͷΈผεϨουʹݶఆ͠ςετ͕༻ҙ
  6. "DUJPOʹΑΔ&GGFDUΛ੔ཧ 3FEVDFS "DUJPO ϝΠϯεϨουͰTUBUFΛߋ৽ & ff FDUΛฦ͢ɻ ෭࡞༻Λ࣮ߦ͢Δ 4UBUF͕ มΘΔͱ7JFXʹ൓ө

    7JFX ؔ৺ࣄ ͦͷػೳͷ७ਮͳॲཧͷΈߦ͍4UBUFΛมߋɻ ʢ෭࡞༻͸ݺͼग़ͯ͠ΔΑ͏ʹݟ͑Δ͕ ໭Γ஋Ͱฦ͠3FEVDFSͰ͸ॲཧͯ͠ͳ͍ʣ ؔ৺ࣄ ֎෦௨৴ͷ࣮ߦ΍ɺ Ӭଓσʔλ࡞੒Λ୲౰͢Δɻ ॲཧͷํ޲
  7. 5$"ͷϞδϡʔϧ෼ׂྫ AppFeature EffectsBasics FactClient FactClientLive AppDelegate/SeneDelegate Host Application OSSͷ௨৴ϥΠϒϥϦ Counter

    ґଘͷ޲͖ ▫ JNQPSU▫ ෭࡞༻Λݺͼग़ؔ͢਺ͷմɻ ߴ֊ؔ਺Λར༻͠ɺ ؔ਺ͷ࣮૷Λ%*Ͱ͖Δɻ ෭࡞༻Ͱ͋Δ࣮ࡍͷ௨৴Λߦ͏ ؔ਺ͷ಺෦࣮૷ ʮؔ৺͝ͱͷ෼཭͸ඞͣ͠΋ΦχΦϯʹ͠ͳͯ͘΋͍͍ʯ 🐲
  8. 3FEVDFSͷϢχοτςετίʔυྫ @MainActor struct EffectsBasicsTests { @Test func numberFact() async {

    // Arrange let store = TestStore(initialState: EffectsBasics.State()) { EffectsBasics() } withDependencies: { $0.factClient.fetch = { "\($0) is a good number Brent" } } // Action await store.send(.incrementButtonTapped) { $0.count = 1 // Assertion } await store.send(.numberFactButtonTapped) { $0.isNumberFactRequestInFlight = true } await store.receive(\.numberFactResponse.success) { $0.isNumberFactRequestInFlight = false $0.numberFact = "1 is a good number Brent" } } "DUJPOޙͷ4UBUFͷظ଴஋Λ૊Έཱ͍ͯͯΔ "DUJPOޙͷ4UBUFͷظ଴஋Λ૊Έཱ͍ͯͯΔ ෭࡞༻࣮ߦޙʹ"DUJPO͕࣮ߦ͞Εɺ ͦͷ"DUJPOޙͷ4UBUFͷظ଴஋Λ૊Έཱ͍ͯͯΔ ςετ༻ʹ%*͢Δ
  9. 5$"ͱؔ਺ܕϓϩάϥϛϯά w खஈ w ෭࡞༻ͷ෼཭ w ߴ֊ؔ਺ʹΑΔؔ਺ͷ%*ʢࠓճ͸ਂ͘۷ΓԼ͍͛ͯ·ͤΜʣ w ߴ֊ؔ਺ʹΑΔؔ਺߹੒ʢࠓճͷ࿩Ͱ͸આ໌Ͱ͖͍ͯ·ͤΜʣ w

    ݁Ռ΍໨త w ςετ͕ॻ͖΍͘͢ͳΔʢؔ৺ࣄ΋෼཭Ͱ͖Δʣ w 5$"ͩͱςετͷϑϨʔϜϫʔΫ΋ఏڙ͞Ε͍ͯΔͨΊಡΈ΍͍͢ w ϓϩμΫγϣϯίʔυ΋༧ଌՄೳੑ͕ߴ͍ͷͰίʔυ͕ԿΛ͍ͨ͠ͷ͔͕Θ͔Δ w ߟ͑Δ΂͖͜ͱ w ͜Ε͕ϕετͳํ๏͔Ͳ͏͔͸ίʔυΛॻ͍ͨΓΈͨΓ͢Δਓͨͪ࣍ୈ w 5$"ͷ੍໿ʹैΘͳ͍৔߹ʹϏϧυΤϥʔͱ͸ͳΒͳ͍ͷͰࣗ༝͞΋͋Γ༧ଌෆՄೳʹͰ͖Δ w ֶशίετ͸ϝϯόʔશһʹ͋ΔͨΊɺνʔϜʹͱͬͯϕετ͔Ͳ͏͔͸Θ͔Βͳ͍
  10. J04ΞϓϦ։ൃͰ5$"Λ࢖͏্ͰڗडͰ͖Δ͜ͱ w ࣗ෼͕ॻ͍ͨίʔυͷৼΔ෣͍Λ༧ଌՄೳʹ͍ͨ͠ w ͦͷͨΊʹ෭࡞༻Λ෼཭͍ͨ͠ w ςετίʔυΛॻ͖΍͍ͨ͘͢͠ಡΈ΍͍ͨ͘͢͠ w ςετίʔυͷॻ͖ํΛ"DUJPOʹରͯ͠4UBUF͕ͲͷΑ͏ʹมԽ͠ɺ& ff

    FDU͕Ͳ͏ϑΟʔυόοΫ͢Δ͔ͷϨʔϧʹԊ͏Α͏ʹ͍ͨ͠ w ෭࡞༻Λ෼཭Ͱ͖͍ͯͳ͍3FEVDFS͸ςετίʔυΛύεͤ͞Δͷ͸೉͍͠ w ʢࠓճઆ໌ͯ͠ͳ͍͚Ͳʣ w ඇಉظॲཧ΍࣌ؒΛ੍ޚͨ͠ςετΛ্खʹॻ͘͜ͱ͸҆ఆͨ͠ςετͷͨΊʹ͸ඞཁͰ5$"Ͱ͸ͦΕ͕Մೳ w ੈͷதͷTQFDΛॻ͍͍ͯ͘ςετͷॻ͖ํ͸शख़౓ʹΑͬͯϒϨΔͷͰʢࢲʹ͸ʣಡΈ΍͍͢ͱ͸ࢥ͑ͳ͍ w ίϯϙʔωϯτΛ࠶ར༻ͯ͠߹੒͠ա౓ʹϩδοΫΛந৅Խ͠ͳ͍ͰࡁΈ΍Γํ͸ίϯηϯαε͕औΓ΍͍͢ w ίϯϙʔωϯτͷ૊Έ߹ΘͤՄೳʹ͢ΔͨΊʹॻ͖ํ΁டংΛ࣋ͨͤΒΕΔ w ྫ͑͹7JFX7JFX.PEFM6TF$BTF3FQPTJUPSZ%BUB4PVSDFͷΑ͏ʹڥքΛଟ͘࡞ΓQSPUPDPM·ΈΕʹͳΒͣʹࡁΉ
  11. ԿͷͨΊʹ5$"Λ࢖͏͔Λҙࣝ͠ͳ͍ͱࠔΔ͜ͱͷํ͕͋Δ w ෭࡞༻Λ෼཭Ͱ͖͍ͯͳ͍3FEVDFS͸ςετίʔυΛॻ͚ͳ͍ w ෭࡞༻Λ෼཭Ͱ͖ͳ͍ύλʔϯͷྫ w ෭࡞༻Λ࣮ߦ͢ΔϩδοΫΛγϯάϧτϯʹ͔ͯͭ͠%*ෆՄʹͯ͠͠·͏ w Կ͕෭࡞༻͔Λ஌Βͳ͍ͱҙຯ͕Θ͔Βͳ͍͸ͣ w

    ݪཧओٛతʹͲΜͳػೳ΋3FEVDFSΛհͯ͠ॲཧΛ࣮ߦ͢΂͖ͱແཧͯ͠͠·͏ w 3FEVDFSΛհͯ͠ॲཧΛ࣮ߦ͢Δඞཁ͸ͳ͘ɺ4XJGU6*ͷΠϕϯτ಺ͰࡁΉγϯϓϧͳ͜ͱ͸γϯϓϧʹ΍Ε͹͍͍ w ঢ়ଶΛ࣋ͭͷ͔࣋ͨͳ͍ͷ͔ɺςετ͢΂͖͔ͦ͏Ͱͳ͍͔Έ͍ͨͳׂΓ੾Γͷηϯε͕ඞཁ w 5$"ͷ߹੒͞Εͨ4UBUF͸ͻͱͭͷͰ͔͍ͬ4UBUFͰ͋ΓͦΕ͕ΞϓϦͷঢ়ଶͰ͋Δ͜ͱΛҙࣝ͠ͳ͍ͱ͍͚ͳ͍ w ྫ͑͹ΞϓϦ಺ͰϨΨγʔϓϩδΣΫτͷҰ෦Λ5$"ʹ͢Δͷ͸΍ΓͮΒ͍ʢؾ͕͢Δʣ w ΞϓϦ಺ͷ5$"ͷ3FEVDFS֎͔Β4UBUFΛ5$"಺ʹ఻ൖͤ͞ΔɺͱͳΔͱσʔλιʔεʢ৘ใݯʣ͕ෳ਺Ͱ445͔Βԕ͔͟Δ w 4XJGUݴޠͷػೳΛ࢖͍·ͬͯ͘୹͘ॻ͚ΔΑ͏ʹਐԽͯ͠ΔͷͰ4XJGUΛ஌Βͳ͍ͱຐ๏ͷΑ͏ʹݟ͑ͯ͠·͏ w ୯ʹ୹͘ॻ͚ΔΑ͏ʹͳͬͯΔ͚ͩͰ௕͘΋ॻ͚Δ͚ͩͳͷʹɺͦͷҙຯΛ἞ΈऔΔͷ͕೉͘͠ࢥͬͯ͠·͏
  12. 3FEVDFSͱ.77..71 3FEVDFS 4XJGU6*7JFX PS 6*,JU6*7JFX 7JFX.PEFM PS 1SFTFOUFS 4XJGU6*7JFX PS

    6*,JU6*7JFX .PEFM PSΈΜͳେ޷͖ ϨΠϠʔυ .77..71͸.PEFMͱ͍͏΋ͷΛͲ͏࡞Δ͔ΛܾΊ͍ͯͳ͍ɻ ͨͱ͑͹6TF$BTF͔3FQPTJUPSZ͔ϨΠϠʔԽΛͨ͘͠ͳΔ༠࿭͸͋Δɻ Ͱ΋6TF$BTF3FQPTJUPSZʹ౰ͯ͸·Βͳ͍ΠϨΪϡϥʔέʔε͸ଟ͍ΑͶʁ ͰɺͲ͏ςετΛॻ͘ͷʁྫ͑͹3FQPTJUPSZύλʔϯΛར༻͢Δͱͯ͠ɺ ΫϥΠΞϯτΞϓϦ͕௨৴͔%#͔ΩϟογϡΛҙࣝ͠ͳ͍ඞཁੑ͸͋Δͷʁ ΫϥΠΞϯτΞϓϦ͸ΩϟογϡΛ໌ࣔతʹ࢖͍͍ͨ৔߹΍࢖͍ͨ͘ͳ͍৔߹Λ ࢦఆ͢ΔέʔεͰ͸Ҿ਺Ͱ੾Γସ͑Δͷʁͦͨ͠ΒϢʔεέʔεʹҾͬுΒΕͯ υϝΠϯ஌͕ࣝ3FQPTJUPSZʹ͍͔ͳ͍Α͏ʹ͢Δֶश͢Δίετ͸ͳ͍ͷʁ & ff FDU 3FEVDFSΑΓઌ͸& ff FDU͚ͩͰ͍͍ɻ & ff FDUSVOͷઌΛෳࡶʹͯ͠΋ϝϦοτΛײ͡ΔλΠϛϯά͸ͳ͍ͱࢥ͏ɻ