初めに
背景
今までPythonを使ってきたのですが、転職先ではJavaとGolangを主に使用することになりました。特にPythonにはない型を意識した開発のメリット、JavaとGolangのintefaceの違いがあまりよく分からなかったので、調べてみました。
この記事では、PythonとJavaとGolangの違いについて触れ、そして記事の最後には GolangとJavaのintefaceを違いを明示しつつ、Pythonで書き直してみました。
対象読者
- 動的型付け言語しか使ったことない人
- JavaとGolangの違いがはっきり分からない人
- 型を意識したことがあまりない人
シンプルな実装
まず、Python, Java, Golangの3つで一番シンプルなクラスを書いてみる。
Pythonは以下を見てもらうと分かる通り、Pythonを知らない人でも、理解できてしまうぐらいシンプル。
Pythonの場合
class Dog:
def __init__(self, name):
self.name = name
def bark(self):
return f"{self.name} : bowwow!"
dog = Dog("ポチ")
print(dog.bark()) # ポチ : bowwow!
Javaだと以下のようになる。
Javaの場合
public class Dog {
private String name;
public Dog(String name) {
this.name = name;
}
public String bark() {
return this.name + " : bowwow!";
}
public static void main(String[] args) {
Dog dog = new Dog("ポチ");
System.out.println(dog.bark()); // ポチ : bowwow!
}
}
次にGolangでかく。
Golangの場合
package main
import "fmt"
type Dog struct {
name string
}
func NewDog(name string) *Dog {
return &Dog{name}
}
func (d *Dog) Bark() string {
return d.name + " : bowwow!"
}
func main() {
dog := NewDog("ポチ")
fmt.Println(dog.Bark()) // ポチ : bowwow!
}
PythonとJavaはオブジェクト指向であるのに対して、Golangはclassがなく伝統的なオブジェクト指向ではない。
型システム
型システムの分類の仕方はいくつかある。
- 動的型付け vs. 静的型付け
- 強い型付け vs. 弱い型付け
- Structural Type System(構造的型付け) vs. Nominal Type System (記名的型付け)
ここでは、まず分かりやすい動的型付け、静的型付けから説明する。
型がない言語
機械語、シェルスクリプトには型の概念がない
動的型付け言語
型の概念はあるが、型の整合性の検査は実行時に行う言語
e.g.) Ruby,Python,Perl,JavaScript,Lispなど
静的型付け言語
型の概念はあり、型の整合性の検査を実行前に行う言語
型検査
型を明示的に記述する必要があり、型の整合性の検査のみが必要な言語
e.g.) C,Java
型推論
型を明示的に記述する必要がなく(しなくてもよい)、型を推論しつつ整合性を検査する必要がある言語
e.g.) OCaml, Haskell, F#
型安全性
プログラムPの型付けが成功していれば、Pの実行時に型の不整合に由来するエラーが起きない性質を満たすことを型安全であるという。(強い型付けともいう)
強い型付け
型の不整合が検出できる。
動的で強い型付け
Python, Rubyなど
a = "1"
b = 5
a+b # エラー
静的で強い型付け
Golang, Javaなど
package main
import "fmt"
func main() {
a := "1"
b := 5
fmt.Println(a + b) // エラー
}
弱い型付け
型の不整合が検出できないときがある。
動的で弱い型付け
PHPやJavaScriptなど
var x = 1
var y = '9'
x + y // '19'
静的で弱い型付け
CやC++など
#include <stdio.h>
int main(void){
int num = 1;
char chr = 'a';
printf("%d", num + chr); //98
}
ダックタイピング
ダックタイピングとは動的型付け言語で用いられる型付けのスタイルで、RubyやPythonなどで使用される。
If it walks like a duck and quacks like a duck, it must be a duck. by Dave Thomas
もしもそれがアヒルのように歩き、アヒルのように鳴くのなら、それはアヒルに違いない
実際にPythonでダックタイピングしてみる。
登場人物 | 説明 |
---|---|
Dog | 吠える犬 |
Bard | 吠えない鳥 |
Robot | 吠えるロボット |
class Dog:
def Bark(self):
print("Bow wow🐕")
class Bird:
def sing(self):
print("Chun chun🐤")
class Robot:
def Bark(self):
print("Woooo🤖")
def roll_call(animal):
if hasattr(animal, 'Bark'):
animal.Bark()
else:
print("This is not animal.❌")
dog = Dog()
bird = Bird()
robot = Robot()
roll_call(dog) # Bow wow🐕
roll_call(bird) # This is not animal.❌
roll_call(robot) # Woooo🤖
birdはBark()を持っていないので、animalではないと判定されたが、それ以外は動物とみなされた。
メリット
ダックタイピングは呼び出すメソッドさえを持っていればいいので、呼び出し側は型などの前提知識は不要である。
- 柔軟性・拡張性が高くなる
- 依存が少なくなる
デメリット
何においてもトレードオフなのでデメリットは存在する。
ダックタイピングでは、特定の関数をたまたま持っていた意図しない型を入れてしまう可能性がある。つまり以下のようなデメリットがある。
- 型の整合性によるエラーが起きやすくなり、危険性が伴う
Nominal Type System(記名的型付け)
Javaのように名前が重要で、部分型関係が明示的に宣言される型システム。
e.g.)C#やJava
Javaでは{ fst: Nat, snd: Nat }
のように名前のない型は宣言できない。
ローカル変数、フィールド、メソッドの仮引数を宣言するときは絶対に名前が必要である。
メリット
- 再帰型の取り扱いが簡単
- 値の型がそれぞれ名前で保存されるだけ
- リフレクションやマーシャリング、画面出力などで活用できる(structuralな型システムでも、値に型を示すタグを埋め込めばいいわけだが、あまり活用されていない。)
- 型検査後も、特に実行時に型情報を活用できる
- 型検査、特にsubtypingの検査が自明なまでに簡単
- 明示的にsubtypingを宣言するため、誤りが生じにくい
- 「偽の包摂関係」を防止できる
- (ある型の値をその型とは完全に別だが、構造的には互換がある型が期待されている場所に用いるようなプログラムを型検査機が拒絶し損ねるという問題)を防止することができる。(ただし、単一コンストラクタデータ型や抽象型などもっといい方法がある)
デメリット
- 名前的型システムでは、型名とその定義についてのテーブルを常に扱う必要があり、そのせいで定義も証明もより冗長となってしまいがち
- 研究ではStructural Subtypingの方が人気
- 発展的な機能(パラメータ多相)、抽象データ型、ユーザ定義の型演算子、ファンクタなどの型抽象に関する強力な仕組み)は、名前的型システムと相性が悪い(List(T)のような型は複合的な型であり、原始的な名前として扱うことができない。List(T)の挙動を見るには、Listの定義を参照する必要がある)
- コードが冗長になりやすい
Nominal Typing
Nominal Typingとは型の名前が同じであれば、互換性がある仕組み。
Nominal Subtyping
Subtypeとは
Subtypeとは部分型のこと。部分型とはis-aの関係にあるもの。オブジェクト指向のクラスの継承に似ている。Subtypeはほとんどがsubstitutability(交換可能性)を持っている。つまり、リスコフの置換原則を満たす。
φ(x) を型 T のオブジェクト x に関して証明可能な性質とする。このとき、φ(y) は型 T のサブタイプ S のオブジェクト y について真でなければならない。 B. Liskov and J. Wing、A Behavioral Notion of Subtyping
もう少し分かりやすく言い換えると
「サブタイプ」のオブジェクトは別の型(「スーパータイプ」)のオブジェクトのすべての振る舞いと、更に別の何かを備えたものである。 ここで必要とされるものは、以下に示す置換の性質のようなものだろう:型 S の各オブジェクト o1 に対し、型Tのオブジェクト o2 が存在し、T に関して定義されたすべてのプログラム P が o1 を o2 で置き換えても動作を変えない場合、S は T のサブタイプである B. Liskov、Data Abstraction and Hierarchy、Liskov 1988, p. 25, 3.3. Type Hierarchy
具体的な例を出すと、以下のJavaの例のようにint <: double
(int型はdouble型の部分型)のとき、int型のオブジェクトT をdobule型に変えても何も問題がないが、double型をint型とみなすのは危険である。
public class Main {
public static void main(String args[]) {
int x = 100;
double y;
y = x;
System.out.println(y); // 100.0
double a = 100;
int b;
b = a;
System.out.println(y); // コンパイルエラー
}
}
Structural Type System(構造的型付け)
名前が本質的ではなく、部分型関係が型の構造の上に直接定義される型システム。(型の構造)
TypeScript, OCaml, MLなど。
最近ではTypeScirptをNominalにする方法がある。
メリット
- 少なくとも再帰型がなければ、構造的型システムのほうが少し整然・洗練されている
- 構造的なほうでは、型式は独立したものとなり、型式の意味を理解するために必要な情報が備わっている
デメリット
- 再帰型が定義しにくい
Structural Typing(構造的部分型)
構造が同じなら同じ型とみなす。
シグネチャが一緒だと同じ型とみなすと言う定義がよく見られるが、
シグネチャの定義はメソッドを識別するための情報であり、nominalな観点からいうシグネチャとstructuralな観点からいうシグネチャの意味は異なる。具体的にいうと、前者のシグネチャはメソッド名を含むが、後者はメソッド名は含まない。
class Dog {
name: string;
constructor(name: string) {
this.name = name;
}
}
class Cat {
name: string;
constructor(name: string) {
this.name = name;
}
}
// 引数にDog型を明示する
const printDogName = (dog: Dog) => console.log(dog.name);
printDogName(new Dog("ポチ")); // OK 👌
printDogName(new Cat("タマ")); // OK 👌 構造が一緒なので同じクラスとみなされる。
Structural Subtyping(構造的部分型)
Structural Subtyping は Static Duck Typingと呼ばれる。
Static Duck Typingについては後に、Golangのinterfaceにおける例を載せている。
ハイブリットな型付け
最近は、Structuralな性質とNominalな性質の両方を持つ言語が多い。
Golang, Scalaなど。
Golang は Strucualなのか?
Golangは一見Nominal Typingに見える。
以下の例では、型の構造が一緒でも型名が違うのでエラーになる。
package main
import "fmt"
func main() {
type t string
var foo t = "abc"
var bar string
fmt.Printf("%T\n", foo) // main.t
fmt.Printf("%T\n", bar) // string
bar = foo // cannot use str1 (variable of type t) as string value in assignment
}
しかし、Javaなどのように明示的に型に命名する必要はなく、以下のように無名な型を用いることもできる。
package main
import "fmt"
func main() {
fullname := struct {
FirstName string
LastName string
}{
FirstName: "John",
LastName: "Doe",
}
fmt.Printf("%T\n", fullname)
}
他の例 : https://go.dev/play/p/Q5wSbFPNbx7
次に、リスコフの置換原則を満たすかどうかだが、GolangではJavaのように暗黙の型変換はできない。
package main
import "fmt"
func main() {
var x int = 100
var y float64
y = x
fmt.Println(y) // エラー
}
そこで、interfaceで考てみる。
Golangでリスコフの置換原則
package main
import "fmt"
type factory interface {
createFace() string
}
type Hero struct {
name string
}
func (h Hero) createFace() string {
return h.name
}
type Anpanman struct {
Hero
punch string
kick string
}
type Shokupanman struct {
Hero
punch string
}
type Batako struct{}
func (Batako) changeFace(f factory) {
fmt.Println(f.createFace(), "新しい顔よ〜!")
}
func main() {
a := Anpanman{
Hero: Hero{name: "アンパンマン"},
punch: "アンパンパンチ",
kick: "アンパンキック",
}
s := Shokupanman{
Hero: Hero{name: "食パンマン"},
punch: "食パンパンチ",
}
b := Batako{}
b.changeFace(a) // アンパンマン 新しい顔よ〜!
b.changeFace(s) // 食パンマン 新しい顔よ〜!
}
Golangでダックタイピング
ところでGolangは動的型付け言語であるにも関わらず、ダックタイピングだとよく言われる。
これは Structural Subtyping(Static Duck Typing)であるからだと思われる。
先ほどPythonで
package main
import "fmt"
type Animal interface {
Bark()
}
type Dog struct{}
func (d Dog) Bark() {
fmt.Println("Bow wow🐕")
}
type Bird struct{}
func (b Bird) sing() {
fmt.Println("Chun chun🐤")
}
type Robot struct{}
func (r Robot) Bark() {
fmt.Println("Woooo🤖")
}
func runBark(animal Animal) {
animal.Bark()
}
func main() {
dog := Dog{}
bird := Bird{}
robot := Robot{}
runBark(dog) // Bow wow🐕
runBark(bird) // エラー❌
runBark(robot) // Woooo🤖
}
Pythonと同じように振る舞いを満たさないbirdだけがエラーになる。これがGolangがDuck Typingと呼ばれる理由である。GolangはNominalでありつつも、Structural Subtyping(Static Duck Typing)なのである。
interface
interfaceはGolangやJavaにはあるが、Pythonには存在しない。
言語によっては、protocol、trait とも呼ばれる。
intefaceを分かりやすく説明した例として契約を例にあげた説明がある。
intefaceはオブジェクトの操作の名前と型の集まりに過ぎない。
interfaceを使うことのメリット&デメリット
メリット
実装の切り替えが可能となる
テストが容易になる
多重継承ができる
デメリット
冗長になる可能性がある。
GolangとJavaのInterfaceの違いとは?
まず、1番大きな違いとして、Javaではimplements
を明示する。
Golangのinterfaceは先ほどのダックタイピングの例のようにstructuralなので、明示する必要がない。
業務では、Javaではinterfaceを書くことを推奨される場合が多い。しかし、Golangの場合は、interfaceを書くことが冗長と思われることが多々ある。
JavaではInterface自身が基底の型のように扱われることでどのようなクラスであるかを表現する。
Golangはstaticなダックタイピングと言われるが、GolangのInterfaceは満たすべき振る舞いを定義し、その振る舞いを満たすかどうかで制約を表現する。
GolangとJavaのinterfaceをPythonで書いてみる
先ほどのシンプルなクラスをinterfaceを使って書いてみる。
ダックタイピングのように振る舞いを表すことを目的とするGolangのinterfaceと明示的なJavaのinterfaceは 同じではない。当然Pythonで書くときも全く同じではない。
Javaのinterface
Javaのinterface
public interface Animal {
String bark();
}
public class Dog implements Animal {
private String name;
public Dog(String name) {
this.name = name;
}
@Override
public String bark() {
return this.name + " : bowwow!";
}
public static void main(String[] args) {
Animal dog = new Dog("ポチ");
System.out.println(dog.bark()); // ポチ : bowwow!
}
}
これはPythonではABCを使って書くことができる。
Pythonで書いたJavaのinterface
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def bark(self):
pass
class Dog(Animal):
def __init__(self, name):
self.name = name
def bark(self):
return f"{self.name} : bowwow!"
dog = Dog("ポチ")
print(dog.bark()) # ポチ : bowwow!
Javaの public class Dog implements Animal
と同じように ABCを使うと class Dog(Animal):
と明示する必要がある。
Golangのinterface
Golangの場合
package main
import "fmt"
type Animal interface {
Bark() string
}
type Dog struct {
name string
}
func NewDog(name string) *Dog {
return &Dog{name}
}
func (d *Dog) Bark() string {
return d.name + " : bowwow!"
}
func main() {
dog := NewDog("ポチ")
fmt.Println(dog.Bark()) // ポチ : bowwow!
}
Golangのinterfaceはstructuralである。先ほどのJavaの例はnominalであった。
これをPythonにはtypingのProtocolを使用する。ProtocolはPythonでStructural Subtyping (Static Duck Typing)を実現する。
Pythonで書いたGolangのinterface
from typing import Protocol
class Animal(Protocol):
def bark(self) -> str:
pass
class Dog:
def __init__(self, name: str):
self.name = name
def bark(self) -> str:
return f"{self.name} : bowwow!"
dog = Dog("ポチ")
print(dog.bark()) # ポチ : bowwow!
というようになる。こちらは ABCと違いclass Dog(Animal):
のように明示する必要はない。
最後に
ここでは記事が長くなりすぎたため、interfaceを使用したDI(依存性の逆転)などの例を省いたが、次の記事ではDIやより実用的な例については触れていきたいと思う。
参考文献
- Benjamin C. Pierce, Types and Programming Languages (The MIT Press) Hardcover – January 4, 2002
- interface(インタフェース)型は、メソッドのシグニチャの集まりで定義しますってどういうこと?
- インターフェースを「契約」として見たときの問題点 ― C#への「インターフェースのデフォルト実装」の導入(前編)
- インターフェース(interface)とは | インターフェースを使用する方法
- インターフェースって何のメリットがあるんですか?
- https://peps.python.org/pep-0544/
- PythonにおけるProtocol(ダックタイピング)とABC(抽象化)の違い
- 【Java】interface (インターフェース) の実用例を考えてみる
- インターフェースはなぜ使うのか?
- なぜGoはDuck typingを採用していると言えるのか、データ構造から詳しく解説してみた
- Goの実装例で理解するダックタイピング
- PythonにおけるProtocol(ダックタイピング)とABC(抽象化)の違い
- Structural Subtyping vs Nominal Subtyping 比較して理解する
- 構造的部分型
- きみは Generics がとくいなフレンズなんだね,または「制約は構造を生む」
- Interface 型をあらかじめ宣言しなくてもよい
- 埋め込み型としての構造体 ( Goは継承を使わない )
- プログラム言語論
- 静的型付けでの型推論と動的型付けでの型チェック
- プログラミング言語ー動的・静的と型の強さ、弱さについてー
- 型推論と型検査、静的な型つけと動的な型つけ、強い型つけと弱い型つけ
- プログラミング言語の比較
- 型検査と型推論の概要
- Go言語のinterfaceとダックタイピングのお話 (オブジェクト指向)
- ダックタイピングは、オブジェクト指向特有の多態なのでしょうか?
- 【入門】Python を書く前に知っておきたいデータ型のあれこれ
- Structural vs. Nominal Type Systems
- Why doesn't Go have variance in its type system?
- PEP 544 – Protocols: Structural subtyping (static duck typing)
- 幽霊型による部分型付けの紹介
- Swiftに息づくstructural types(構造的型)
- Goはオブジェクト指向言語だろうか?