ハッキングバカ

プログラミングに関するメモ

Clojure で チャットアプリを作る [Websocket]

環境: macOS 10.13.6, Leiningen 2.8.1 on Java 1.8.0_112

Clojure の Web フレームワークである Luminus を使えば Websocket を用いたチャットアプリが手軽に実現でき、パフォーマンスもすぐれているらしい。
まずは

$ lein new luminus chat

でプロジェクトを作成。
サーバサイドの Websocket コードを配置。

; src/clj/chat/routes/websocket.clj

(ns chat.routes.websocket
  (:require [compojure.core :refer [GET defroutes wrap-routes]]
            [clojure.tools.logging :as log]
            [immutant.web.async :as async]))

(defonce channels (atom #{}))

(defn connect! [channel]
  (log/info "channel open")
  (swap! channels conj channel))

(defn disconnect! [channel {:keys [code reason]}]
  (log/info "close code:" code "reason:" reason)
  (swap! channels #(remove #{channel} %)))

(defn notify-clients! [channel msg]
  (doseq [channel @channels]
    (async/send! channel msg)))

(def websocket-callbacks
  "WebSocket callback functions"
  {:on-open connect!
   :on-close disconnect!
   :on-message notify-clients!})

(defn ws-handler [request]
  (async/as-channel request websocket-callbacks))

(defroutes websocket-routes
  (GET "/ws" [] ws-handler))

これを handler から呼び出す。

; src/clj/chat/handler.clj
(ns chat.handler
  (:require [chat.middleware :as middleware]
            [chat.layout :refer [error-page]]
            [chat.routes.home :refer [home-routes]]
            [chat.routes.websocket :refer [websocket-routes]] ; ここと
            [compojure.core :refer [routes wrap-routes]]
            [ring.util.http-response :as response]
            [compojure.route :as route]
            [chat.env :refer [defaults]]
            [mount.core :as mount]))

(mount/defstate init-app
  :start ((or (:init defaults) identity))
  :stop  ((or (:stop defaults) identity)))

(mount/defstate app
  :start
  (middleware/wrap-base
    (routes
      websocket-routes ; ここを追加
      (-> #'home-routes
          (wrap-routes middleware/wrap-csrf)
          (wrap-routes middleware/wrap-formats))
          (route/not-found
             (:body
               (error-page {:status 404
                            :title "page not found"}))))))

html ファイル。デフォの home を変更。

<!-- resource/templates/home.html -->
<input type="text" placeholder="メッセージ" id="chat_form">
<input  type="button" value="送信" id="chat_send">
<div id="messages"></div>
<script src="/js/ws.js"></script>

クライアントサイドの js ファイルを作成。公式では clojurescript を使っているがここではプレーン js を使う。
また、直接 public/js 配下においているがきちんとしたものを作るときは別のディレクトリでやるべきだろう。

// resource/public/js/ws.js
let socket = new WebSocket("ws://" + location.host + "/ws");

socket.onmessage = function(event) {
	let chat = JSON.parse(event.data)["chat"];
	let messages = document.getElementById("messages");
	let first_child = messages.firstChild;
	let msg = document.createElement("div");
	msg.innerHTML = chat
	messages.insertBefore(msg, first_child);
}

window.onload = function () {
	document.getElementById('chat_send').addEventListener('click',
		(function () { socket.send(JSON.stringify({chat: document.getElementById('chat_form').value}));
			document.getElementById('chat_form').value=""}), false);
}

これで完成。

$ lein run

でサーバを起動。
http://localhost:3000/ にアクセスする。
複数のブラウザを立ち上げてメッセージが同期していることを確認。
f:id:hackbaka:20180803171851p:plain
参考: Luminus - a Clojure web framework

Clojure で論理プログラミング

環境 macOS 10.13.6
ClojureProlog 的な論理プログラミングをする場合は、GitHub - clojure/core.logic を使う。
とりあえず

$ lein new logic

project ファイルに書き加える。

; project.clj

(defproject logic "0.1.0-SNAPSHOT"
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [org.clojure/core.logic "0.8.11"]]
  :main logic.core)

依存関係を更新

$ lein deps

wikiっぽく例の一家でやってみる

; src/logic/core.clj

(ns logic.core
  (:require [clojure.core.logic.pldb :as pldb]
            [clojure.core.logic :as l]))

; 関係を定義
(pldb/db-rel parent p c)

; 事実を挙列
(def facts
  (pldb/db
    [parent 'namihei 'sazae]
    [parent 'fune 'sazae]
    [parent 'namihei 'katsuo]
    [parent 'fune 'katsuo]
    [parent 'namihei 'wakame]
    [parent 'fune 'wakame]
    [parent 'sazae 'tara]
    [parent 'masuo 'tara]))

(defn -main []
  (pldb/with-db facts
    (println "波平の子供:")
    (println (l/run* [q]
                     (parent 'namihei q)))
    (println "タラの親")
    (println (l/run* [q]
                     (parent q 'tara)))
    (println "フネの孫")
    (println (l/run* [q]
              (l/fresh [x]
                       (parent 'fune x)
                       (parent x q))))))

動かしてみる

$ lein run
波平の子供:
(sazae katsuo wakame)
タラの親
(masuo sazae)
フネの孫
(tara)

面白い。
もっといじってみよう。

参考:
core.logic/tests.clj at master · clojure/core.logic · GitHub
clojure - From defrel and facts to core.logic.pldb - Stack Overflow

whenever で cron を設定する[Rails]

環境 Ubuntu 16.04, Rails 5.2.0, Ruby 2.5.1

cron で定期的なバッチ処理を行う場合の設定。
まずは Gemfile に whenever を追加。

# Gemfile
gem 'whenever', '0.10.0', :require => false

インストール

$ bundle update

設定ファイルの生成

$ wheneverize .

config/schedule.rb が生成されるので、そこにスケジュールを記入する。
ここでは本番環境で毎日0:00にヘルパーメソッドを実行することにする。

# config/schedule.rb
env :PATH, ENV['PATH']
set :environment,  "production"
every 1.day, :at => '0:00 am' do
  runner "ExamplesHelper.example_batch"
end

以下のコマンドで反映。

$ whenever --update-crontab

以下のコマンドで設定を確認できるが、元の cron の見方を知っていないと難しい。

$ whenever

参考: Railsで定期的にバッチ回す「Whenever」

入れ子になった Model に pagination を適用する[Rails]

環境: Rails 5.2.0
Topic モデルが Comment モデルを has_many で持つとき、topic の show ページで comments を paginate したいときがある。
View で paginate を呼び出すことで対応できる。

# app/views/topics/show.html.erb
<% comments = @topic.comments.paginate(page: params[:page]) %>
<%= render comments %>
<%= will_paginate comments %>

has_many で持つ Model の数を validate する[Rails]

環境: rails 5.2.0
Shop モデルが has_many で Product モデルを持つとき、その個数を1~32個の間に指定するような validation が欲しい。
一つは自分で作ること。
参考: Rails Model で has_many 定義した要素数を validate する方法 - Shred IT!!!!

# app/model/shop.rb
...
validate :products_number
...
private

  def products_number
    errors.add(:products, "を1つ以上指定して下さい") if products.size < 1
    errors.add(:products, "は32個までです") if products.size > 32
  end

これで問題はないのだけれど良くありそうな問題だしもっと良い解決法はないのかと思ったら length が使えるらしい。
参考: ruby on rails - Validate that an object has one or more associated objects - Stack Overflow

# app/model/shop.rb
validates :products, length: { minimum: 1, maximum: 32 }

こっちの方がシンプルに書けるのだが、length は基本的に文字数が前提のようでエラーメッセージが「商品は1文字以上必要です」のように違和感のある日本語になってしまう。
辞書ファイルをいじるのは面倒なので、一つ目の方を使う事にする。

has_many で持つ子 Model の特定のカラムの和を求める [Rails]

分かりにくい題名になってしまったが、例えば User モデルが has_many で複数の Book モデルを持つとき、ある user が持つ books の price の和を求めたいとする。

 user.books.sum(:price)

で求まる。
参考: activerecord - Rails has_many :through sum attribute on "child objects" --> SQL Toughy - Stack Overflow