🤖

ReduxとSwiftUIの類似性をAIに示してもらった

2022/12/07に公開

React×ReduxSwiftUIによるアプリケーション開発は、共にSingle Source of Truthを掲げ、宣言的な仮想DOM/Viewを用いるなど、類似点が多く見られます。両者はの対象はそれぞれWebフロントエンド・iOSアプリと異なりますが、両者の対応をコードで示すことができれば、より抽象的なレベルで概念を理解する助けになりそうです。

そこで、今話題のAIであるChatGPTに、両者を比較するコードを書いてもらいました。いきなり想定したような形にはならなかったので、対話を重ねて徐々に修正する形で最終形にたどり着きました。対話によって軌道修正できるのが素晴らしいですね。

以下、AIとの会話をそのまま掲載します。🧑が僕の発言で、🤖がAIの発言です。ここまで意思疎通ができることに驚きました。

AIとの対話

🧑

ReduxとSwiftUIは似ていますが、両者の対応をコードの例を挙げて説明して下さい。


🤖

ReduxとSwiftUIは完全に異なるものです。Reduxは、Webフロントエンド開発でよく使われるJavaScriptのライブラリであり、アプリケーションの状態を管理するためのツールです。一方、SwiftUIは、AppleのiOS、macOS、tvOS、およびwatchOSプラットフォーム向けのUIフレームワークです。両者の間には直接的な対応はありません。


🧑

両者はSingle Source of Truthを基本とした状態管理を行う点で似ています。


🤖

そうですね。Reduxは、アプリケーションの状態を単一の管理するための「単一の真実」と呼ばれる概念を基本としています。SwiftUIでは、ほとんど同じような概念があります。SwiftUIでは、View Hierarchyと呼ばれる仕組みを使用して、UIを定義します。このView Hierarchyは、構成されるすべてのViewが含まれる単一の入力から始まります。そして、そのView Hierarchyの中では、状態を管理するために、@Stateプロパティラッパーを使用します。

具体的な例としては、以下のようになります。

// Redux
const initialState = {
  count: 0
};

function reducer(state = initialState, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      return state;
  }
}
// SwiftUI
struct ContentView: View {
  @State var count = 0

  var body: some View {
    VStack {
      Button("Increment") {
        self.count += 1
      }
      Button("Decrement") {
        self.count -= 1
      }
      Text("Count: \(count)")
    }
  }
}

Reduxでは、reducer関数が「単一の真実」となる状態を管理する役割を果たします。一方、SwiftUIでは、@StateプロパティラッパーがView Hierarchyの中で状態を管理する役割を果たします。

また、Reduxでは、アクションを通じて状態を更新しますが、SwiftUIでは、View Hierarchyが変化することによって状態が更新されます。

ちなみに、@Stateプロパティラッパーを使用して状態を管理することは、SwiftUIでのみ可能です。Reduxのようなライブラリを使用しない限り、@Stateプロパティラッパーを使用することはできません。


🧑

Reduxの場合の例は、Reactを使ったものにしてもらえますか?


🤖

もちろんです。Reduxを使用するには、Reactやその他のJavaScriptフレームワークと組み合わせることができます。例えば、以下のようになります。

import React from 'react';
import { createStore } from 'redux';

// Redux
const initialState = {
  count: 0
};

function reducer(state = initialState, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      return state;
  }
}

const store = createStore(reducer);

// React
class Counter extends React.Component {
  state = {
    count: store.getState().count
  };

  componentDidMount() {
    store.subscribe(() => {
      this.setState({
        count: store.getState().count
      });
    });
  }

  increment = () => {
    store.dispatch({ type: 'increment' });
  };

  decrement = () => {
    store.dispatch({ type: 'decrement' });
  };

  render() {
    return (
      <div>
        <button onClick={this.increment}>Increment</button>
        <button onClick={this.decrement}>Decrement</button>
        <div>Count: {this.state.count}</div>
      </div>
    );
  }
}

上記の例では、Reduxを使用して状態を管理し、Reactを使用してUIを定義しています。Reduxでは、reducer関数が「単一の真実」となる状態を管理する役割を果たし、Reactでは、CounterコンポーネントがUIを定義する役割を果たしています。

この例では、Reduxのstoreオブジェクトに状態を保存し、store.getState()を使用してその状態を取得し、store.dispatch()を使用してアクションを定義し、store.subscribe()を使用して状態が更新されたときにUIを更新するように設定しています。

ReduxとReactを組み合わせることで、状態をより簡単に管理できるようになります。また、Reduxのような状態管理ライブラリを


🧑

SwiftUIの例は、@StateObjectを使う形にして下さい。


🤖

もちろんです。SwiftUIでも、@Stateプロパティラッパーを使用して状態を管理することができます。また、@StateObjectプロパティラッパーを使用することで、アプリケーション内で共有される状態を管理することができます。

例えば、次のようになります。

struct Counter {
  @State var count: Int = 0
}

struct ContentView: View {
  @StateObject var counter = Counter()

  var body: some View {
    VStack {
      Button("Increment") {
        self.counter.count += 1
      }
      Button("Decrement") {
        self.counter.count -= 1
      }
      Text("Count: \(self.counter.count)")
    }
  }
}

上記の例では、Counterという名前のデータ型を定義しています。このデータ型は、@Stateプロパティラッパーを使用して状態を管理します。そして、ContentViewという名前のViewを定義しています。このViewは、@StateObjectプロパティラッパーを使用してCounterデータ型のインスタンスを保持します。

これにより、ContentViewが持つ@StateObjectプロパティであるcounterは、ContentViewのサブビューからアクセスできるようになります。counterプロパティを使用して、countプロパティを更新することで、状態を更新することができます。

@StateObjectプロパティラッパーを使用することで、アプリケーション内で共有される状態を管理することができます。これにより、状態をより簡単に管理できるようになります。


🧑

@StateObjectは、ObservableObjectに準拠したクラスに対してしか使えません。Counter構造体に対して@StateObjectを使うとコンパイルエラーになります。


🤖

その通りです。@StateObjectプロパティラッパーは、ObservableObjectに準拠したクラスに対してのみ使用することができます。つまり、次のようなコードでは、コンパイルエラーになります。

struct Counter {
  @State var count: Int = 0
}

struct ContentView: View {
  @StateObject var counter = Counter() // Compile error: 'Counter' does not conform to protocol 'ObservableObject'
  
  // ...
}

この場合は、Counter構造体をObservableObjectプロトコルに準拠させることで、@StateObjectプロパティラッパーを使用することができます。例えば、次のようになります。

class Counter: ObservableObject {
  @Published var count: Int = 0
}

struct ContentView: View {
  @StateObject var counter = Counter()

  // ...
}

上記の例では、CounterクラスをObservableObjectプロトコルに準拠させることで、@StateObjectプロパティラッパーを使用することができるようになりました。

@StateObjectプロパティラッパーを使用する場合は、必ずObservableObjectに準拠したクラスを使用する必要があることに注意してください。そうしないと、コンパイルエラーになる可能性があります。


🧑

Reduxの例に対応するように、Counterクラスにincrementメソッドとdecrementメソッドを追加して下さい。


🤖

もちろんです。以下のように、Counterクラスにincrementメソッドとdecrementメソッドを追加することで、Reduxの例に対応することができます。

class Counter: ObservableObject {
  @Published var count: Int = 0

  func increment() {
    self.count += 1
  }

  func decrement() {
    self.count -= 1
  }
}

struct ContentView: View {
  @StateObject var counter = Counter()

  var body: some View {
    VStack {
      Button("Increment") {
        self.counter.increment()
      }
      Button("Decrement") {
        self.counter.decrement()
      }
      Text("Count: \(self.counter.count)")
    }
  }
}

上記の例では、Counterクラスにincrementメソッドとdecrementメソッドを追加しました。これにより、ContentView内でcounter.increment()counter.decrement()といった形で、Counterクラスの状態を更新することができるようになりました。

また、@Publishedプロパティラ


🧑

Reduxの方の例は、Redux Toolkitを使用してもらえますか?


🤖

もちろんです。Redux Toolkitを使用することで、Reduxをより簡単に使用することができます。例えば、以下のようになります。

import { createSlice } from '@reduxjs/toolkit';

// Redux Toolkit
const initialState = {
  count: 0
};

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment(state) {
      state.count += 1;
    },
    decrement(state) {
      state.count -= 1;
    }
  }
});

const store = configureStore({
  reducer: counterSlice.reducer
});

// React
class Counter extends React.Component {
  state = {
    count: store.getState().count
  };

  componentDidMount() {
    store.subscribe(() => {
      this.setState({
        count: store.getState().count
      });
    });
  }

  increment = () => {
    store.dispatch(counterSlice.actions.increment());
  };

  decrement = () => {
    store.dispatch(counterSlice.actions.decrement());
  };

  render() {
    return (
      <div>
        <button onClick={this.increment}>Increment</button>
        <button onClick={this.decrement}>Decrement</button>
        <div>Count: {this.state.count}</div>
      </div>
    );
  }
}

上記の例では、Redux Toolkitを使用しています。Redux Toolkitを使用すると、createSlice関数を使用して、reducerとactionを同時に定義することができます。これにより、Reduxの設定をより簡潔にすることができます。

また、Redux Toolkitを使用することで、configureStore関数を使用してstoreを作成することができます。これにより、Reduxのstoreを作成するための設定をより簡単にすることができます。

上記の例では、counterSliceという名前のcreateSlice関数で定義されたスライスを使用しています。このスライスには、incrementdecrementという2つのreducerが定


🧑

Reactでは、Function Componentを使うようにして下さい。


🤖

もちろんです。Function Componentを使用することで、Reactのコンポーネントをより簡潔に書くことができます。例えば、次のようになります。

import { createSlice } from '@reduxjs/toolkit';
import { useSelector, useDispatch } from 'react-redux';

// Redux Toolkit
const initialState = {
  count: 0
};

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment(state) {
      state.count += 1;
    },
    decrement(state) {
      state.count -= 1;
    }
  }
});

const store = configureStore({
  reducer: counterSlice.reducer
});

// React
function Counter() {
  const count = useSelector((state) => state.count);
  const dispatch = useDispatch();

  function increment() {
    dispatch(counterSlice.actions.increment());
  }

  function decrement() {
    dispatch(counterSlice.actions.decrement());
  }

  return (
    <div>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
      <div>Count: {count}</div>
    </div>
  );
}

上記の例では、Counterという名前の関数を定義しています。この関数をFunction Componentと呼びます。Function Componentを使用することで、Reactのコンポーネントをより簡潔に書くことができます。

また、Function Componentを使用する場合は、useSelectoruseDispatchという2つの関数を使用することができます。useSelector関数を使用することで、Reduxのstoreからstateを取得することができます。また、useDispatch関数を使用することで、Reduxのstoreに対してactionをdispatchすることができます。

上記の例では、useSelector関数を使用してstateからcountを取得しています。また、useDispatch関数を使用してincrementdecrementという2つのactionをdispatchしています。これにより、Function Component内でReduxのstoreからstateを取得したり、actionをdispatchすることができるようになります。

Function

まとめ

このようにして、AIと対話しながら、React×ReduxとSwiftUIの対応を示すコードサンプルを作り上げることができました。完成したコードを見比べると、actionとreducerがObservableObjectメソッドに、Reactの仮想DOMがSwiftUIのViewに対応していることがわかります。

ChatGPTはまだまだ荒削りだとは感じますが、対話を通して文脈を理解し、こちらの意図を汲んで修正を重ねてくれるのは本当に素晴らしいです。これからさらに発展していくだろうことを考えると、今後欠かせないツールになるのは間違いないと思います。

Discussion