Nimを使ってGUIアプリケーションを作成してみる

今回のあらすじ

前回の記事、「Nim言語を使って簡単に文章の類似度を計算してみる」の続きになります。
今回は文章の類似度を計算するGUIアプリケーションに挑戦していきます。

NiGUI

NiGUIは「cross-platform desktop GUI toolkit」ということで、
Windows, Mac, Linuxで動作するGUI用のライブラリです。
https://github.com/trustable-code/NiGui

アプリケーションを作成する

NiGUIのインストール

nimble install nigui

Macの場合

MacでNiGUIを利用しようとすると、
could not load: libgtk-3.0.dylib
と怒られることがあります。

その場合はbrewなどで必要なライブラリを入れてあげます。

brew install libgtk

準備

せっかくなので前回のコードを使いまわしましょう。
Nim言語を使って簡単に文章の類似度を計算してみるのコードを「ngram.nim」、
今回のコードを「nigui_test.nim」として以下のように保存します。

f:id:taanatsu:20210830183349p:plain

ngram.nimの中身

一応コピペができるように前回のコードをこちらに記載しておきます。
まだ読んでない方はぜひNim言語を使って簡単に文章の類似度を計算してみるを読んでみてくださいね!

import unicode
import tables
import math

proc createNGram*(n: int, text: string): seq[string] =
    ##
    ## n-gramデータを作成します
    ## 
    ## n: n-gramのnに当たる数値
    ## text: n-gramに分解(コーパス)したい文字列
    
    # マルチバイト文字列を扱えるようにテキストをルーン化する(Goとかにもある、アレ)
    let runeText = text.toRunes()

    # 何文字目まで連結させたかを保持しておく変数(Runeは1文字ずつ扱うため、indexでどこまで扱ったかをカウントしておく)
    var index = 0
    # n-gramで何文字ずつの文字列(コーパス)にするかを決めるためのカウント変数
    var cnt = 0
    # n-gramでの文字列を作成する際に利用するtmp変数
    var tmp: string

    while true:
        # cntがn-gramの指定文字数を超えたらそこで切り出す(安全のため<=としている)
        if n <= cnt:
            # resultは暗黙変数(nimでは返り値を定義すると自動的にresult変数が生成される)
            result.add(tmp)

            tmp = ""
            cnt = 0

            # n-gramの特性上一つ前の文字をもう一度利用するので、n-1をしている
            index = index - (n - 1)
            
        
        if text.runeLen() <= index:
            break
        
        # 1文字ずつ連結していく
        tmp = tmp & $runeText[index]

        cnt = cnt + 1
        index = index + 1


proc tf*(corpus: seq[string]): Table[string, int] =
    ##
    ## コーパスの中のTFを計算します
    ## 
    ## corpus: コーパスが格納されたseq配列を指定します

    for c in corpus:
        # 連想配列にその単語があれば1加算、なければその連想配列のキーを作成し、1を代入
        if result.hasKey(c):
            result[c] += 1
        else:
            result[c] = 1

proc cosineSimilarity*(text1: string, text2: string, ngramNum: int): float =
    ##
    ## 文章の類似度を調べます
    ## 
    ## text1: 1つ目の文章
    ## text2: 2つ目の文章
    ## ngramNum: 何gramにテキストを分解するか
    ##

    # 文章をそれぞれコーパスに分解します
    let text1Copus = createNGram(ngramNum, text1)
    let text2Copus = createNGram(ngramNum, text2)

    # text2のTF値を求めます
    let text2Tf = tf(text2Copus)

    # コサイン類似度の計算に必要な分子分母の変数
    var c = 0.0
    var m1 = 0.0
    var m2 = 0.0

    for t1c in text1Copus:
        # text2のコーパスにtext1のコーパスがあるかないかで類似度を計算することにします
        # text2のコーパスにtext1のコーパスがあれば1、なければ0を使います
        var n = 0.0
        if text2Tf.hasKey(t1c):
            n = 1.0
        
        # コサイン類似度に利用する分子分母の数値を計算
        c += (1 * n)
        m1 += 1 * 1
        m2 += n * n
    
    # コサイン類似度の計算
    if m1 == 0 or m2 == 0:
        return 0
    result = c / round(sqrt(m1) * sqrt(m2))

GUIの作成

NiGUIを使ってGUIを作成していきます。

import nigui

app.init()

# ウインドウの作成
var window = newWindow("テキストの類似度を計算する")
# ウインドウのサイズを設定
window.width = 600.scaleToDpi
window.height = 265.scaleToDpi

# ボタンなどを表示する領域の作成
var container = newContainer()
window.add(container)

# テキストエリアを作成
var textArea1 = newTextArea()
container.add(textArea1)
textArea1.x = 0
textArea1.y = 0
textArea1.width = 290
textArea1.height = 200

# テキストエリアを作成
var textArea2 = newTextArea()
container.add(textArea2)
textArea2.x = 310
textArea2.y = 0
textArea2.width = 290
textArea2.height = 200

# 類似度計算ボタンを作成
var calcButton = newButton("類似度の計算")
container.add(calcButton)
calcButton.x = 480
calcButton.y = 220
calcButton.width = 100
calcButton.height = 35

window.show()
app.run()

上記のコードを実行してみます。

$ nim c -r nigui_test.nim

すると、以下のような画面が生成されます。
f:id:taanatsu:20210830183542p:plain

これでGUIの基盤ができました。

ボタンを押したら類似度を計算し、表示させる

Nim言語を使って簡単に文章の類似度を計算してみるのコード(ngram.nim)を読み込みます。

import ngram

次にボタン押下時の処理を追加します。

# 類似度計算ボタン押下時の処理
calcButton.onClick = proc(event: ClickEvent) =
  let textArea1Text = textArea1.text
  let textArea2Text = textArea2.text

  # 2つの文章の類似度を計算
  let similarity = ngram.cosineSimilarity(textArea1Text, textArea2Text, 2)

  # メッセージボックスで、計算した類似度を表示
  window.alert("2つの文章の類似度は" & $similarity & "です。")

これで準備が完了です。
全体のコードを以下に記載いたします。

import nigui
import ngram


app.init()

# ウインドウの作成
var window = newWindow("テキストの類似度を計算する")
# ウインドウのサイズを設定
window.width = 600.scaleToDpi
window.height = 265.scaleToDpi

# ボタンなどを表示する領域の作成
var container = newContainer()
window.add(container)

# テキストエリアを作成
var textArea1 = newTextArea()
container.add(textArea1)
textArea1.x = 0
textArea1.y = 0
textArea1.width = 290
textArea1.height = 200

# テキストエリアを作成
var textArea2 = newTextArea()
container.add(textArea2)
textArea2.x = 310
textArea2.y = 0
textArea2.width = 290
textArea2.height = 200

# 類似度計算ボタンを作成
var calcButton = newButton("類似度の計算")
container.add(calcButton)
calcButton.x = 480
calcButton.y = 220
calcButton.width = 100
calcButton.height = 35

# 類似度計算ボタン押下時の処理
calcButton.onClick = proc(event: ClickEvent) =
  let textArea1Text = textArea1.text
  let textArea2Text = textArea2.text

  # 2つの文章の類似度を計算
  let similarity = ngram.cosineSimilarity(textArea1Text, textArea2Text, 2)

  # メッセージボックスで、計算した類似度を表示
  window.alert("2つの文章の類似度は" & $similarity & "です。")


window.show()
app.run()

動作

f:id:taanatsu:20210830183438p:plain

おわりに

さて、一通りソフトウェアの開発ができました。
Nimはなかなかおもしろい言語ですので、よかったらはまってみてください!
(Excite内でもはまっている人もいます!)

では、また次回!