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

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

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers. →
Avatar for yimajo yimajo
June 15, 2025

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

Avatar for yimajo

yimajo

June 15, 2025

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ͷઌΛෳࡶʹͯ͠΋ϝϦοτΛײ͡ΔλΠϛϯά͸ͳ͍ͱࢥ͏ɻ