M.Hiroi's Home Page

Clojure Programming

お気楽 Clojure プログラミング超入門


Copyright (C) 2025 Makoto Hiroi
All rights reserved.

例外処理

一般に、例外 (exception) はエラー処理で使われる機能です。「例外=エラー処理」と考えてもらってもかまいません。最近は例外処理を備えているプログラミング言語が多くなりました。もちろん Clojure にも例外処理があります。なお、エラーが発生したことを「例外が発生した」とか「例外が送出された」という場合もあります。本稿でもエラーのことを例外と記述することにします。

●例外の捕捉

通常、例外が発生すると Clojure はプログラムの実行を中断しますが、致命的な例外でなければプログラムの実行を継続する、または特別な処理を行わせたい場合もあるでしょう。このような場合にこそ、例外処理が役に立つのです。Clojure では発生した例外を捕まえるのに、Java と同様の try / catch / finally を使います。try /catch の構文を下図に示します。

(try
  body
  (catch 例外A 引数 処理A ...)
  (catch 例外B 引数 処理B ...)
  ...
)

        図 : 例外処理

try は、そのあとに定義されている body を評価します。body が正常に終了した場合は try も終了します。もしも、body で例外が発生した場合、body の実行は中断され、その例外が catch 節で指定した例外と一致すれば、その catch 節を実行します。なお、try には複数の catch 節を指定することができます。

catch 節には捕捉する例外 (Java のクラス) を指定します。Java の場合、例外は Throwable というクラスとして定義されています。例外は階層構造になっていて、すべての例外は直接または間接的に Throwable を継承します。Throwable は Error と Exception に分けられ、Exception は RuntimeException とそれ以外の例外に分けられます。

Error を継承した例外は、復旧するのが困難なエラーが発生したことを表します。RuntimeException を継承した例外は、Java の仮想マシン (JVM) で発生したエラーを表します。たとえば、0 で割ったときに送出される例外 ArithmeticException や、配列の添字が範囲外であることを表す例外 ArrayIndexOutOfBoundsException などがあります。

●try の使い方

try の使い方は簡単です。次の例を見てください。

user=> (try (/ 1 2) (catch ArithmeticException e (println (.getMessage e)) 0))
1/2

user=> (try (/ 1 0) (catch ArithmeticException e (println (.getMessage e)) 0))
Divide by zero
0

Clojure (Java) の場合、0 で除算すると例外 ArithmeticException を送出して実行を中断します。ここで、try の catch 節に ArithmeticException を指定すると、例外を捕捉して処理を続行することができます。

1 / 2 は 1/2 を返しますが、1 / 0 は 0 で除算しているので例外 ArithmeticException が送出されます。この例外クラスは catch 節に指定されているので、その節が実行されてエラーメッセージを表示して 0 を返します。エラーメッセージは Java のメソッド getMessage で取得することができます。

●例外の送出

例外は throw で送出することができます。

(throw (ExceptionClass. args ...))

throw には例外クラスのインスタンスを引数として渡します。Clojure の場合、例外クラス ExceptionClass のインスタンスは ExceptionClass. で生成することができます。throw が実行されると、プログラムの実行を直ちに中断して、例外を受け止める catch 節があると、そこへ制御が移ります。該当する catch 節がない場合、プログラムの実行は中断されます。

簡単な例を示しましょう。

user=> (try (throw (RuntimeException. "oops")) 
(catch Exception e (println (.getMessage e))))
oops
nil

例外に渡した引数は、例外クラスのインスタンスに格納されます。例外クラスのインスタンスは try の catch 節で受け取ることができます。上記の例では、送出された例外のインスタンスは変数 e にセットされます。例外に渡したメッセージはインスタンスに格納されます。

●ex-info と ex-data

エラー情報を Clojure のデータとして渡したい場合は ex-info を使うと簡単です。

ex-info message map

ex-info は文字列 message とマップ map を受け取り、例外クラス ExceptionInfo のインスタンスを生成します。ex-info が生成したインスタンスからデータを取り出すには ex-data を使います。引数が ex-info で生成されたインスタンスであれば、ex-data はマップを返します。それ以外のインスタンスであれば nil を返します。エラーメッセージは .getMessage で取得できます。

簡単な例を示しましょう。

user=> (def a (ex-info "oops" {:foo 10}))
#'user/a

user=> (type a)
clojure.lang.ExceptionInfo

user=> a
#error {
 :cause "oops"
 :data {:foo 10}
 :via
 [{:type clojure.lang.ExceptionInfo
   :message "oops"
   :data {:foo 10}
   :at [clojure.lang.AFn applyToHelper "AFn.java" 156]}]
 :trace
 ・・・略・・・}

user=> (.getMessage a)
"oops"
user=> (ex-data a)
{:foo 10}

user=> (try (throw (ex-info "oops" {:foo 10, :bar 20})) 
(catch Exception e (println (ex-data e))))
{:foo 10, :bar 20}
nil

●大域脱出

Clojure (Java) の例外は、try の中で呼び出した関数の中で例外が送出されても、それを捕捉することができます。この機能を使って、評価中の関数からほかの関数へ制御を移す「大域脱出 (global exit)」を実現することができます。

簡単な例を示しましょう。

user=> (defn bar1 [] (println "call bar1"))
#'user/bar1
user=> (defn bar2 [] (throw (Exception. "Global Exit")))
#'user/bar2
user=> (defn bar3 [] (println "call bar3"))
#'user/bar3
user=> (defn foo [] (bar1) (bar2) (bar3))
#'user/foo

user=> (try (foo) (catch Exception e (println (.getMessage e))))
call bar1
Global Exit
nil

実行の様子を下図に示します。

 ┌───────┐
 │(try  ...     │←─┐
 │ (catch ... ))│    │
 └───────┘    │
        ↓             │
 ┌──────┐      │
 │   (foo)    │──┐│
 └──────┘    ││
       ↓↑          ↓│
 ┌──────┐  ┌ (bar2) ────────┐ 
 │  (bar1)    │  │(throw (Exception. ...))│
 └──────┘  └────────────┘

            図 : 大域脱出

通常の関数呼び出しは、呼び出し元の関数に制御が戻ります。ところが bar2 で throw が実行されると、呼び出し元の関数 foo を飛び越えて、制御が try の catch 節に移るのです。このように、例外処理を使って関数を飛び越えて制御を移すことができます。

大域脱出はとても強力な機能ですが、多用すると処理の流れがわからなくなる、いわゆる「スパゲッティプログラム」になってしまいます。使用には十分ご注意下さい。

●finally 節

ところで、プログラムの途中で例外が送出されると、残りのプログラムは実行されません。このため、必要な処理が行われない場合があります。このような場合、try に finally 節を定義します。finally 節は try の処理で例外が発生したかどうかにかかわらず、try の処理が終了するときに必ず実行されます。例外が発生した場合は、finally 節を実行したあとで同じ例外を再送出します。

なお、catch 節と finally 節を同時に try に書く場合は、catch 節を先に定義してください。そのあとで finally 節を定義します。

簡単な例を示しましょう。大域脱出で作成した foo を呼び出す関数 baz を作ります。

user=> (defn baz [] (try (foo) (finally (println "clean up"))))
#'user/baz

実行すると次のようになります。

user=> (try (baz) (catch Exception e (println (.getMessage e))))
call bar1
clean up
Global Exit
nil

関数 bar2 で送出された例外 Exception は baz の finally 節で捕捉されて print 'clean up' が実行されます。その後、例外 Exception が再送出され、REPL で実行した try の catch 節に捕捉されます。

●逆ポーランド記法

それでは簡単な例題として、逆ポーランド記法で書かれた数式を計算するプログラムを作りましょう。私達が普通に式を書く場合、1 + 2 のように演算子を真ん中に置きます。この書き方を「中置記法 (infix notation)」といいます。中があれば前と後もあるだろうと思われた方はいませんか。実はそのとおりで、「前置記法 (prefix notation)」と「後置記法 (postfix notation)」という書き方があります。

前置記法は演算子を前に置く書き方で、ポーランド記法 (Polish Notation) と呼ばれることもあります。たとえば、1 + 2 であれば + 1 2 と書きます。前置記法を採用したプログラミング言語といえば Lisp が有名です。数式にカッコをつけてみると (+ 1 2) となり、Lisp のプログラムになります。

後置記法は演算子を後ろに置く書き方で、逆ポーランド記法 (RPN : Reverse Polish Notation) と呼ばれることもあります。1 + 2 であれば 1 2 + のように書きます。逆ポーランド記法の利点は、計算する順番に演算子が現れるため、カッコが不要になることです。たとえば、1 と 2 の和と 3 と 4 の和との積という数式を表してみましょう。

中置記法: (1 + 2) * (3 + 4)
後置記法: 1 2 + 3 4 + *

逆ポーランド記法は、日本語の読み方とまったく同じです。1 2 + で 1 と 2 の和を求め、3 4 + で 3 と 4 の和を求め、最後に 2 つの結果を掛け算して答えが求まります。

私達は中置記法に慣れているため、逆ポーランド記法はわかりにくいのですが、コンピュータで利用する場合、演算ルーチンが簡単になるという利点があります。実際、Forth というプログラミング言語は数式を逆ポーランド記法で表します。

●逆ポーランド記法の計算

逆ポーランド記法の数式はスタックを使うと簡単に計算することができます。アルゴリズムは次のようになります。

  1. 数値はスタックに追加する。
  2. 演算子であればスタックから 2 つ数値を取り出し、演算結果をスタックに追加する。
  3. 最後にスタックに残った値が答えになる。

たったこれだけの規則で数式を計算することができます。それでは、実際に 1 2 + 3 4 + * を試してみましょう。次の表を見てください。

表 : 計算過程
数式操作スタック
1PUSH[ 1 ]
2PUSH[ 2, 1 ]
+POP (2)[ 1 ]
POP (1)[ ]
1+2=3[ ]
PUSH[ 3 ]
3PUSH[ 3, 3 ]
4PUSH[ 4, 3, 3 ]
数式操作スタック
+POP (4)[ 3, 3 ]
POP (3)[ 3 ]
3+4=7[ 3 ]
PUSH[ 7, 3 ]
*POP (7)[ 3 ]
POP (3)[ ]
3*7=21[ ]
PUSH[ 21 ]

スタックは [ ] で表しています。最初の 1 と 2 は数値なのでスタックにプッシュします。次は演算子 + なので、スタックからデータを取り出して 1 + 2 を計算します。そして、計算結果 3 をスタックにプッシュします。次に、3 と 4 は数値なのでスタックにプッシュします。その次は演算子 + なので同じように処理して、計算結果 7 をスタックにプッシュします。

スタックの中身は [ 7, 3 ] となり、最初の計算結果 3 と次に計算した結果 7 がスタックに格納されています。この状態で最後の * を処理します。7 と 3 を取り出すとスタックは空の状態になります。そして、3 * 7 を計算して 21 をスタックにプッシュします。これで計算は終了です。スタックに残っている値 21 が計算結果となります。

このように、スタックを使うことで逆ポーランド記法で書かれた数式を簡単に計算することができます。実は数式だけではなく、スタックを用いてプログラムを実行することもできます。プログラミング言語 Forth は「数値」と「ワード」という 2 種類のデータしかありません。ワードには +, -, *, / などの演算子のほかに、いろいろな処理が定義されています。もちろん、ユーザが新しいワードを定義することもできます。

Forth の動作は、数値であればスタックにプッシュして、ワードであればそれを実行する、というシンプルなものです。これでプログラミングができるのですから、とてもユニークな言語ですね。

●演算子の定義

それでは、逆ポーランド記法の数式を計算するプログラムを作ってみましょう。最初に、演算子を定義します。次のリストを見てください。

リスト : 演算子の定義

(def operator
  {
   "+" (fn [a b] (+ a b)),
   "-" (fn [a b] (- a b)),
   "*" (fn [a b] (* a b)),
   "/" (fn [a b] (/ a b))
  })

演算子 +, -, *, / の処理は無名関数で行います。各演算子に対応する無名関数はマップ operator に登録しておきます。

●数式の計算処理

次は、逆ポーランド記法を計算する関数 calc-rpn を作ります。

リスト : 数式の計算

(defn calc-rpn [buff]
  (loop [stack '()
         expr (split buff #" ")]
    (if (seq expr)
      (let [n (parse-double (first expr))]
        (if n
          (recur (cons n stack) (rest expr))
          (let [op (get operator (first expr))]
            (if-not op
              (throw (ex-info "operator not found" {:op (first expr)}))
              (if (< (count stack) 2)
                (throw (ex-info "stack underflow" {:op (first expr), :stack stack}))
                (let [m (op (second stack) (first stack))]
                  (recur (cons m (rest (rest stack))) (rest expr))))))))
      (if (== (count stack) 1)
        (first stack)
        (throw (ex-info "expression error" {:expr buff :stack stack}))))))

calc-rpn は引数 buff に文字列を受け取ります。数字と演算子は空白で区切ることにします。clojure.string の関数 split で与えられた文字列を空白で分解します。split は区切り文字を正規表現で指定します。#" " は空白文字を表します。正規表現は回を改めて説明する予定です。

簡単な使用例を示しましょう。

user=> (require '[clojure.string :refer [split]])
nil
user=> (split "1 2 + 3 4 - *" #" ")
["1" "2" "+" "3" "4" "-" "*"]

あとは loop / recur で数式 expr から要素を取り出して、スタック stack を使って計算を行います。数式 expr が空でなければ、先頭要素を関数 parse-double で浮動小数点数に変換します。変換できない場合は nil を返します。簡単な実行例を示します。

user=> (parse-double "12345")
12345.0
user=> (parse-double "1.2345")
1.2345
user=> (parse-double "1e300")
1.0E300
user=> (parse-double "abcd")
nil

n が nil でなければ、それをスタック stack に積んで次の要素を調べます。そうでなければ、大域変数 operator から演算子を探します。見つからない場合は throw でエラーを送出します。次に、演算子 op を実行しますが、このとき stack にデータが 2 つ以上あることを確かめます。データが無ければ throw でエラーを送出します。

op を呼び出すときは first, second で stack からデータを取り出して渡します。Clojure のリストは immutable なので、破壊的な操作はできないことに注意してください。返り値は変数 m にセットし、それを stack に積みます。このとき、rest を 2 回適用して、先頭から 2 つの要素を取り除きます。

計算が終了したら、スタックに残っている値を返します。計算が正常に終了した場合、stack にはデータが一つしか残っていないはずです。そうでなければ throw でエラーを送出します。

●数式の入力処理

最後に、数式を入力するための簡単なプログラムを作ります。

リスト : 数式の入力

(defn -main []
  (print "RPN CALC\n> ")
  (flush)
  (loop [buff (.readLine *in*)]
    (when buff
      (try
        (println (calc-rpn buff))
        (catch Exception e (println (.getMessage e) (ex-data e))))
      (print "> ")
      (flush)
      (recur (.readLine *in*)))))

標準入力 *in* から .readLine で数式を読み込み、それを calc-rpn に渡して計算します。もしも例外が送出された場合、try の catch 節で捕捉されて変数 e に例外のインスタンスがセットされます。あとは println でエラーメッセージを表示します。これで例外が送出されても処理を続けることができます。プログラムの終了は、Windows であれば Ctrl-Z を、UNIX 系の OS であれば Ctrl-D を入力してください。

●簡単な実行例

それでは実行してみましょう。

$  clj -M -m rpn
RPN CALC
> 1 2 + 5 3 - *
6.0
> 1 2 + *
stack underflow {:op *, :stack (3.0)}
> 1 2 3 +
expression error {:expr 1 2 3 +, :stack (5.0 1.0)}
> 1 2 **
operator not found {:op **}
>

正常に動作していますね。このように、逆ポーランド記法の数式はスタックを使って簡単に計算することができます。そして、try / catch で例外を捕捉することにより、例外が送出されても処理を継続することができます。


●プログラムリスト

;;;
;;; rpn.clj : 逆ポーランド記法
;;;
;;;           Copyright (C) 2025 Makoto Hiroi
;;;
(ns rpn
  [:require [clojure.string :refer [split]]])

;; 演算子
(def operator
  {
   "+" (fn [a b] (+ a b)),
   "-" (fn [a b] (- a b)),
   "*" (fn [a b] (* a b)),
   "/" (fn [a b] (/ a b))
  })

;; 数式の計算
(defn calc-rpn [buff]
  (loop [stack '()
         expr (split buff #" ")]
    (if (seq expr)
      (let [n (parse-double (first expr))]
        (if n
          (recur (cons n stack) (rest expr))
          (let [op (get operator (first expr))]
            (if-not op
              (throw (ex-info "operator not found" {:op (first expr)}))
              (if (< (count stack) 2)
                (throw (ex-info "stack underflow" {:op (first expr), :stack stack}))
                (let [m (op (second stack) (first stack))]
                  (recur (cons m (rest (rest stack))) (rest expr))))))))
      (if (== (count stack) 1)
        (first stack)
        (throw (ex-info "expression error" {:expr buff :stack stack}))))))

;; 数式の入力
(defn -main []
  (print "RPN CALC\n> ")
  (flush)
  (loop [buff (.readLine *in*)]
    (when buff
      (try
        (println (calc-rpn buff))
        (catch Exception e (println (.getMessage e) (ex-data e))))
      (print "> ")
      (flush)
      (recur (.readLine *in*)))))

パズル Four Fours

●問題の説明

Four Fours は数字を使ったパズルです。いろいろなルールがあるのですが、今回は簡易ルールで行きましょう。それでは問題です。

数字 4 を 4 つと+, -, ×, ÷, (, ) を使って、答えが 1 から 10 になる式を作ってください。数字は 4 だけではなく、44 や 444 のように合体させてもかまいません。また、-を符号として使うことは禁止します。

数字の 4 を 4 つ使うので Four Fours という名前なのだと思います。ところで、このルールでは 11 になる式を作ることができません。ほかのルール、たとえば小数点を付け加えると、次のように作ることができます。

4 ÷ .4 + 4 ÷ 4 = 11

今回は簡易ルールということで、小数点を使わないで 1 から 10 までの式を作ってください。

●プログラムの作成

それではプログラムを作りましょう。Four Fours の場合、4 つの数値に 3 つの演算子だけなので、数式のパターンは次に示す 5 種類しかありません。

(1) (4 Y 4) X (4 Z 4)
(2) 4 X (4 Y (4 Z 4))
(3) ((4 Z 4) Y 4) X 4
(4) 4 X ((4 Z 4) Y 4)
(5) (4 Y (4 Z 4)) X 4

あとは、X, Y, Z に演算子 +, -, *, / を入れて数式を計算すればいいわけです。Four Fours は数字を合体できるので、数字が 3 つで演算子が 2 つ、数字が 2 つで演算子がひとつ、というパターンもあります。演算子がひとつの場合は簡単ですね。演算子が 2 つの場合は、次の式になります。

(A) (a Y b) X c
(B) a X (b Y c)

a, b, c が数字で X, Y が演算子を表しています。数字は 4 か 44 になります。この場合、a, b, c の組み合わせを生成する必要があります。組み合わせを (a, b, c) で表すと、(4, 4, 44), (4, 44, 4), (44, 4, 4) の 3 通りとなります。これと演算子の組み合わせにより数式を生成して、答えを求めてチェックします。

●数式の生成

これらの数式を Clojure でプログラムすると次のようになります。

リスト : 数式

;; 数式を表すレコード型
(defrecord Expr [left op right])

;; 式の生成
(defn make-expr3 [x y z]
  [(->Expr (->Expr 4 y 4) x (->Expr 4 z 4))
   (->Expr 4 x (->Expr 4 y (->Expr 4 z 4)))
   (->Expr (->Expr (->Expr 4 z 4) y 4) x 4)
   (->Expr 4 x (->Expr (->Expr 4 z 4) y 4))
   (->Expr (->Expr 4 y (->Expr 4 z 4)) x 4)])

(defn make-expr2 [x y a b c]
  [(->Expr (->Expr a y b) x c)
   (->Expr a x (->Expr b y c))])

最初に、数式を表すレコード型 Expr を定義します。Expr は二分木と同じデータ構造で、left が左辺式、op が演算子、right が右辺式を表します。関数 make-expr3 は (1) から (5) の数式を生成し、ベクタに格納して返します。引数 x, y, z が演算子を表します。これらの引数はシンボル +, - *, / を渡します。make-expr2 は数式 (A) と (B) を生成します。引数 a, b, c は数値を表します。

●数式の計算

数式の計算も簡単です。次のリストを見てください。

リスト : 数式の計算

(defn calc-expr [expr]
  (if (number? expr)
    expr
    (let [{:keys [left op right]} expr]
      (case op
        + (+ (calc-expr left) (calc-expr right))
        - (- (calc-expr left) (calc-expr right))
        * (* (calc-expr left) (calc-expr right))
        / (/ (calc-expr left) (calc-expr right))))))

(defn eval-expr [expr]
  (let [n (try
            (calc-expr expr)
            (catch ArithmeticException _ 0))]
    (when (and (integer? n) (<= 1 n 10))
      (print-expr expr)
      (println " =" n))))

実際の処理は関数 calc-expr で行います。expr が数値型であれば expr をそのまま返します。引数 expr が Expr 型の場合、calc-expr を再帰呼び出しして、左辺式と右辺式を計算します。あとは case で演算子に対応した計算を行います。case はまだ説明していなかったので、ここで簡単に説明しておきましょう。

case は cond とよく似ています。次の図を見てください。

(case キーとなるS式
  値A S式A
  値B S式B

  ・・・・・

  値M S式M
  :default))

 図 : case の構文

case は最初にキーとなる S 式を受け取り、そのあと cond と同様に複数の節が続きます。cond には節の先頭に条件部がありましたが、case の場合は値を設定します。まず、キーとなる S 式を評価します。次に、この評価結果と節の値を比較します。このとき、値は評価されないことに注意してください。もし、等しい値を見つけた場合は、その節の S 式を実行します。キーと等しい値が見つからない場合、最後の節を無条件で実行します。最後の節が定義されていない場合、case は例外を送出します。

関数 eval-expr は calc-expr を呼び出して数式 expr を評価します。Clojure は 0 で除算すると例外が送出されるので、それを try / catch で捕捉します。その場合は 0 を返します。あとは n が整数値で、かつ 1 以上 10 以下であれば、関数 print-expr で数式を表示します。

●解法プログラムの作成

解法プログラムは次のようになります。

リスト : Four Forus の解法

;; 4が4つある場合
(defn search-four []
  (let [op '(+ - * /)]
    (doseq [x op]
      (doseq [y op]
        (doseq [z op]
          (doseq [expr (make-expr3 x y z)]
            (eval-expr expr)))))))

;; 数字が3つある場合
(defn search-three [a b c]
  (let [op '(+ - * /)]
    (doseq [x op]
      (doseq [y op]
        (doseq [expr (make-expr2 x y a b c)]
          (eval-expr expr))))))

;; 数字が2つある場合
(defn search-two [a b]
  (let [op '(+ - * /)]
    (doseq [x op]
      (eval-expr (->Expr a x b)))))

;; Four Fours の解法
(defn solver []
  (search-four)
  (search-three 4 4 44)
  (search-three 4 44 4)
  (search-three 44 4 4)
  (search-two 4 444)
  (search-two 444 4)
  (search-two 44 44))

関数 search-four は演算子が 3 つの数式を計算して、結果が 1 以上 10 以下の整数値であれば、その式と値を表示します。演算子の組み合わせは「重複順列」と同じです。今回は doseq の 3 重ループで実装しました。4 番目の doseq で 5 つの数式を生成します。それから、演算子が 2 つの数式を計算する search-ttree と演算子が 1 つの数式を計算する search-two を作成します。

あとのプログラムは簡単なので説明は割愛させていただきます。詳細はプログラムリストをお読みください。

●実行結果

それでは実行結果を示します。

$ clj -M -m fours
((4 + 4) - (4 - 4)) = 8
(4 + (4 + (4 - 4))) = 8
(((4 - 4) + 4) + 4) = 8
(4 + ((4 - 4) + 4)) = 8

  ・・・略・・・

((4 / (4 / 4)) / 4) = 1
((44 / 4) - 4) = 7
((44 - 4) / 4) = 10
(44 / 44) = 1

実際に実行すると 100 通りの数式が出力されます。このプログラムでは重複解のチェックを行っていないので、多数の式が出力されることに注意してください。1 から 10 までの数式をひとつずつ示しましょう。

((4 - 4) + (4 / 4)) = 1
((4 / 4) + (4 / 4)) = 2
(((4 + 4) + 4) / 4) = 3
(4 + (4 * (4 - 4))) = 4
(((4 * 4) + 4) / 4) = 5
(((4 + 4) / 4) + 4) = 6
(4 + (4 - (4 / 4))) = 7
((4 + 4) + (4 - 4)) = 8
((4 + 4) + (4 / 4)) = 9
((44 - 4) / 4) = 10

この中で、10 になる式は (44 - 4) / 4 しかありません。数字 4 を 4 つと+, -, ×, ÷, ( , ) だけでは、10 になる式を作ることはできないのですね。興味のある方はいろいろ試してみてください。


●プログラムリスト

;;;
;;; fours.clj : Four Fours
;;;
;;;             Copyright (C) 2025 Makoto Hiroi
;;;
(ns fours)

;; 式
(defrecord Expr [left op right])

;; 式の表示
(defn print-expr [expr]
  (if (number? expr)
    (print expr)
    (let [{:keys [left op right]} expr]
      (print "(")
      (print-expr left)
      (printf " %s " op)
      (print-expr right)
      (print ")"))))

;; 式の評価
(defn calc-expr [expr]
  (if (number? expr)
    expr
    (let [{:keys [left op right]} expr]
      (case op
        + (+ (calc-expr left) (calc-expr right))
        - (- (calc-expr left) (calc-expr right))
        * (* (calc-expr left) (calc-expr right))
        / (/ (calc-expr left) (calc-expr right))))))

(defn eval-expr [expr]
  (let [n (try
            (calc-expr expr)
            (catch ArithmeticException _ 0))]
    (when (and (integer? n) (<= 1 n 10))
      (print-expr expr)
      (println " =" n))))

;; 式の生成
(defn make-expr3 [x y z]
  [(->Expr (->Expr 4 y 4) x (->Expr 4 z 4))
   (->Expr 4 x (->Expr 4 y (->Expr 4 z 4)))
   (->Expr (->Expr (->Expr 4 z 4) y 4) x 4)
   (->Expr 4 x (->Expr (->Expr 4 z 4) y 4))
   (->Expr (->Expr 4 y (->Expr 4 z 4)) x 4)])

;; 4が4つある場合
(defn search-four []
  (let [op '(+ - * /)]
    (doseq [x op]
      (doseq [y op]
        (doseq [z op]
          (doseq [expr (make-expr3 x y z)]
            (eval-expr expr)))))))

;; 式の生成
(defn make-expr2 [x y a b c]
  [(->Expr (->Expr a y b) x c)
   (->Expr a x (->Expr b y c))])

;; 数字が3つある場合
(defn search-three [a b c]
  (let [op '(+ - * /)]
    (doseq [x op]
      (doseq [y op]
        (doseq [expr (make-expr2 x y a b c)]
          (eval-expr expr))))))

;; 数字が2つある場合
(defn search-two [a b]
  (let [op '(+ - * /)]
    (doseq [x op]
      (eval-expr (->Expr a x b)))))

;; Four Fours の解法
(defn solver []
  (search-four)
  (search-three 4 4 44)
  (search-three 4 44 4)
  (search-three 44 4 4)
  (search-two 4 444)
  (search-two 444 4)
  (search-two 44 44))

;; 実行
(defn -main [] (solver))

初版 2025 年 7 月 16, 18 日