見出し画像

Retro-gaming and so on

Javaはそんなに「正しい」言語ではない

丁度タイムリーに教えて!gooの方にJavaに関連する質問があがっていた。
それを見ながらJavaに関するアレコレを考察してみよう。

どうしてPythonをJavaのように書いたらいけないのか

もっと輪郭がハッキリすると思われる。

では問題を見てみる。

/**
* 設問3: 総和を求めよ。引数に渡された文字列は、半角の0〜9で構成される任意長の文字列とする("05963")。各文字を数として扱い、総和を求めて返せ。
* なお空文字列の場合はゼロ(0)とする。数でない文字(アルファベットや記号類)は入らないものとする。
*
* 例: ex3("05963") -> 23 // 5 + 9 + 6 + 3 = 23 ex3("99999") -> 45 ex("") -> 0
* // 空文字列は0
*
* @param text 半角数字で構成された文字列
* @return 計算結果(0以上)
*/

さぁ、一体これをJavaでどう書くべきなのか。

関数型言語の発想だととにかく「データをどのようにそのまま加工するか」考えるが、手続き型言語だとかオブジェクト指向言語だととにかくfor文でデータから一つ一つ要素を取り出して加工する、って事を考えるだろう。
後者の手は2021年現在に於いては愚策なのだ。とにかくforwhileに頼るのはメンド臭い、と感じないといけないのだ。
関数型言語な人のこの感覚は現代人とネアンデルタール人の間にあるくらいの差である。ネアンデルタール人なら「メンド臭い」なんて感じないだろう。
とか言えば怒る人もいるだろうな(笑)。
でも、仮に

ループを使う代わりに繰り返しの基本であるジャンプまたはgoto文を使って書くべきだ

とか言われれば「そりゃ違うだろ」と言うだろ?現代人から見ればそれはネアンデルタール人とアウストラロピテクスの間にある差である。現代人から見るとどっちもどっちだが、ネアンデルタール人から見ればさすがにアウストラロピテクスと一緒にはされたくねぇだろう。「goto文なんてメンド臭くて使ってらんねぇよ」と。
そういう事だ(笑)。分かる?
2021年現在、必要なのはデータをバラしながら操作するんじゃなくって、データを纏めて扱う枠組みなのだ。しかし、Javaは長い間(2014年まで)その機能を欠いていた。それだけの話なのだ。

さて、Javaでどう書くべきか。
僕が書いた解法だと次のようになる。

import java.util.Arrays;
import java.util.List;
import java.util.stream.*;

public class Main {
 public static void main(String[] args) {
  System.out.println(ex3("05963"));
  System.out.println(ex3("99999"));
  System.out.println(ex3(""));
 }
 public static int ex3(String text) {
  int result;
   if ("".equals(text)) {
    result = 0;
   } else {
    result = Arrays.asList(text.split("(?!^)")).stream()
      .map(s -> Integer.valueOf(s))
      .reduce((accum, value) -> accum + value).get();
   }
  return result;
 }
}

満足する結果じゃないが、それでも恐らくfor文やらwhile文やら使って書いたコードよりも短く書けてるとは思う。
そして「満足する結果じゃない」と言うのは、Java特有のストリーム型導入の為の型変換があまりにもメンド臭いから、だ。こんなに頻繁に型変換をする必要があるのか、と言うJavaの言語設計への疑念がある、と言う意味なのだ。
そう、Java 8以降の関数型言語の機能取り込みは後付故にクッソメンド臭い型変換を要してるように見える。結果そこまでスマートには思えないのだ。
大体、Oracleが提供してるStreamの説明がワケワカメである。何をおっしゃってるんだかサッパリ分からん。
一般的に、関数型言語でStreamと言えば遅延評価を前提としたリストが連想される(連想される、と言うのはこれまた厳密な定義ではないから、だ)。そのStreamの事を指してるのか、Oracleの説明を見てもサッパリ、である。
まぁ、こんな説明だと、よっぽど尖った人じゃないと使わないだろう。そしてそもそもJava自体が「平均的なプログラマが使う」と言う前提で設計されてる。尖った人はJavaを使わんのだ。そして平均的な人々がOracleの説明読んて

納得した!早速使ってみよう!

とかなるのだろうか。うむむむむむ。

なお、上の問題をPythonで書いたらアッサリ解けるだろう。
と言うか、僕が書いたコードはアタマの中だと「Pythonだとこう書くから・・・」と言う理屈で書いたからだ。そこには「Pythonの方が正しい」と言う前提がある。
あとで説明するが、JavaはPython程「正しく」ない。逆なのだ。

from functools import reduce

def ex3(text):
 return reduce(lambda acc ,val: acc + val, map(int, text), 0)

Pythonだとまずこれだけ短く書ける。前書いたmapreduceと言う高階関数のシンプルな練習問題にしか過ぎないのだ。

ハッキリ言うけど、プログラミング言語では同じ作業をするなら

書く量が短ければ正義

なのだ。例外はあるだろう。しかしまずはこれを肝に銘じるべきだと思う。
多分あんまピンと来ないと思うんだけど、例えばボーランドがブイブイ言わせてた頃、C言語とPascalの間で決着が付かなかったのはハッキリ言えばこれが原因だ。同じ作業をするにせよ、C言語とPascalで書くコード量に大して差が無かったんだな。結局、マイクロソフトがC言語系にテコ入れしなきゃPascalが負ける事は無かったのだ。
Javaの解題とPythonでの解題を比較してみても、Javaはメソッドだけ単一で書く事は出来ない。Javaではオブジェクト指向を強要される仕様の為、そもそもクラスで包まないとならない。Pythonだとそんなんどこ吹く風、である。

>>> ex3("05963")
23
>>> ex3("99999")
45
>>> ex3("")
0
>>>

ところで、Pythonに慣れた人だと、こう書いた方がシンプルなんじゃないか?と疑問に思うかもしれない。

def ex3(text):
     return sum([int(i) for i in text])

それは正しい。が、逆に言うと、ちょっとJavaに合わせたのだ。Javaだとリスト内包表記が無いし、sumを取るのにもまたもや型変換が必要となる。ワケが分からん仕様なのだ。
こう書くか、

import java.util.Arrays;
import java.util.List;
import java.util.stream.*;

public class Main {
 public static void main(String[] args) {
  System.out.println(ex3("05963"));
  System.out.println(ex3("99999"));
  System.out.println(ex3(""));
 }
 public static int ex3(String text) {
  int result;
  if ("".equals(text)) {
   result = 0;
  } else {
   result = Arrays.asList(text.split("(?!^)")).stream()
     .map(s -> Integer.valueOf(s))
     .collect(Collectors.summingInt(Integer::intValue));
  }
  return result;
 }
}

あるいはこう書かないとならない。

import java.util.Arrays;
import java.util.List;
import java.util.stream.*;

public class Main {
 public static void main(String[] args) {
  System.out.println(ex3("05963"));
  System.out.println(ex3("99999"));
  System.out.println(ex3(""));
 }
 public static int ex3(String text) {
  int result;
  if ("".equals(text)) {
   result = 0;
  } else {
  result = Arrays.asList(text.split("(?!^)")).stream()
    .map(s -> Integer.valueOf(s))
    .mapToInt(Integer::intValue).sum();
  }
  return result;
 }
}

JavaだとまずはIntStreamと言う型がStreamとはまた別にあり、和を取るメソッドはそれじゃないと用意されてないのだ・・・何でやねん。floatとかの和は取れないのか。不思議な仕様である。
collectと言うメソッドも「streamの後始末」と言うか、実体化させる為だけに存在してるようだ。元のコードのget()なんつーのもそれ故に不格好なのだ。何だろうな、これは。

一般的にJavaの方が「正しい」と思われてる模様だが、その誤解の根拠は大体次の二点に絞られている。

  1. JavaはCやC++のようなコンパイル型言語である。
  2. JavaはCやC++のような静的型付け言語である。
2番はさておき、1番が誤解だ、と言うのは以前にも書いた事がある。現代ではコンパイル型言語とかインタプリタ型言語、と言う区分けは存在しない。PythonもRubyも実際は内部で書かれたソースコードをヘーキでコンパイルしているし、Java仮想マシン(JVM)の正体は単なるインタプリタである。
2番に付いては保留しておこう。ハッキリ言うと、個人ユースに於いては趣味の問題で、バグが少ないコードが書ける確率が高い、と言う事で静的型付けを愛好する人がそれなりに多く、この辺の流行りは10〜20年周期でコロコロと変わってきている。
ただし、素早くプログラムを書き上げる、と言う意味だとPythonやRubyのような動的型付けの方が一応は優秀だろう。静的に型を宣言する、ってのはどのみちメンド臭いのだ(※)。従って個人的には、静的型付けのメリットと言うのは、プログラマ側よりもユーザー側の為の仕組みのような気がしている。
それらを考え合わせると、PythonやRubyの方が「より簡単に」「より短く」プログラムを書けるので、Javaより優秀で「より正しい」と言う事が出来ると思う。
これを聞いた人は

「PythonやRubyはJavaより後発なんで、優秀なのは当然だろ」

とか思うかもしれない。
でもそれも誤解である。
Pythonの登場は1991年だ、と言う話を以前した。Rubyの登場は1995年、Javaの登場は実験版を除けば1996年である。実はこの3つの言語ではJavaが一番新しく、一方、オブジェクト指向のアイディアはPythonとRubyがJavaより早く一般向けに実装しており、Javaは新しく登場した割には一番機能的には貧弱な言語だったのだ。
一体これはどういう事なのか、と言うと、Javaは最初っから「もっとマシなC++を作れないか」と言う割合近視眼的な発想でデザインされた言語だったからだ。言い換えると既存のCユーザーやC++ユーザーを取り込もうとする発想しかなかったので、そこから逸脱出来なかったと言う初期設計の限界があった、ってだけの話にしか過ぎない。
一方、PythonもRubyもそういった近視眼的な設計を最初から意図してなく、初期から関数型言語の研究成果を取り込むような設計をしていた。だからJavaに比べるとJavaで言うコレクション型等との親和性が高い設計をしてる。最初っからPythonとRubyは「尖るべくして尖る」設計目標があったので、後付の関数型言語機能を取り込んだJavaより優秀なのは当たり前なのである。
じゃあ、Javaが登場してすぐ人気が出たのは凄い、って思うかもしれないけれど、それはJavaを発表したSun Microsystemsの宣伝結果に過ぎない。
一方、PythonやRubyは草の根でジワジワと人気が出た、と言う「本当の実力がある」言語なのだ。
Pythonは特に日本では「最近人気が出た」から、新しい言語のように思われてるが、元々日本語処理にちと難があって避けられてたのが、3.0でその辺が解決したから、最近登場したように「見える」だけなのだ。しかも、最初の3.0系列が登場したのは2008年で、実はここまで人気が出るまで10年以上の時間を要している。
結局、「宣伝無しで」プログラミング言語が市場に受け入れられて人気が出るには莫大な時間がかかるんだよ、と言うそれだけの話である(C言語さえ1972年の登場から「プログラムを書くならまずはC言語」となる、Windows95辺りの登場まで、20年以上かかってるのだ!)

というわけで、Javaに比べるとPythonやRubyの方が「より正しい」のは当たり前で、Java(あるいはC#)はPythonやRubyを一種「追っかけてる」ようなカタチとなっている。
が。
先にも書いた通り、元々基本設計が近視眼的な事もあり、プログラミング初心者向けとしては向いてない言語だよな、とか正直思っている。スジが悪いのだ。
プログラミングがはじめてなのにJavaを使わせられる学生とか、可愛そうだよな、とか個人的にはかなり同情してはいるのである。

※: 静的型付けでの伝家の宝刀には、プログラマが明示的に変数の型宣言をしなくてもプログラミング言語の方で面倒を見てくれる「型推論」と言う機能がある。
が、これが一般化するにはまだ時間がかかるかもしれない。

  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

最近の「プログラミング」カテゴリーもっと見る

最近の記事
バックナンバー
人気記事