ログ日記

作業ログと日記とメモ

GHC8.4とGHCJS

github.com
ここのコメントにDockerを置いてくれている人が居る。


tarをダウンロードして、stack.yamlを書いて、stack setupしてみる。

.stack-work/dist/x86_64-linux/Cabal-2.2.0.1/build/Parser.hs:1445:48: error
Parser.hs" "Not in scope: type variable ‘a’

こんなエラーが出た。


どうやら
Can't build `ghc-8.8` branch with `happy-1.19.10` (#16652) · Issues · Glasgow Haskell Compiler / GHC · GitLab
Fix optSemi type in Parser.y (071bef18) · Commits · Glasgow Haskell Compiler / GHC · GitLab
この辺りのバグっぽい。
一行書き換えれば良いんだろうけど、stack build途中でエラーになるのでどうすれば良いのか分からない。


仕方がないので、コンパイルしている長い時間の間に

.stack/programs/x86_64-linux/ghcjs-0.2.0.9011009_ghc-8.4.1/src/ghc/compiler/parser/Parser.y

このファイルが生成されているので、stack buildの途中でエラーが出るプロセスに行く前に手動で書き換えた。
普通はどうするんだろう…?


# 追記
グローバルのhappyをダウングレードするために、プロジェクトのresolverが効いている場所でもう一度stack install happy でバージョン1.19.9を入れればエラーにならない?どうもstackのグローバルプロジェクトのbinの使い方がよくわからない。



なんだかGHCJS開発でinteroを使いたかっただけなのにめっちゃ苦労した。

GHCJSのスタブを使うのは大変っぽい

昨日のghc-base-stubを使う方式だと上手くいかなかった。
GHCJSに再度トライ - ログ日記


例えばghcjs-domを使いたい場合、GHCJSだと ghcjs-dom, ghcjs-dom-jsffi を使うところが
GHC版だと ghcjs-base-stub, ghcjs-dom, ghcjs-dom-jsaddle, jsaddle-dom, jsaddle を使うことになる。

これで一旦はコンパイルが通るんだけども、GHCJS版とGHC版でDOMの型が違うっぽい。


GHCJS版
https://raw.githubusercontent.com/ghcjs/ghcjs-dom/master/ghcjs-dom-jsffi/src/GHCJS/DOM/Types.hs

type JSM = IO

-- | This is the same as 'JSM' except when using ghcjs-dom-webkit with GHC (instead of ghcjs-dom-jsaddle)
type DOM = IO

-- | The 'MonadJSM' is to 'JSM' what 'MonadIO' is to 'IO'.
--   When using GHCJS it is 'MonadIO'.
type MonadJSM = MonadIO

-- | This is the same as 'MonadJSM' except when using ghcjs-dom-webkit with GHC (instead of ghcjs-dom-jsaddle)
type MonadDOM = MonadIO

-- | The 'liftJSM' is to 'JSM' what 'liftIO' is to 'IO'.
--   When using GHCJS it is 'liftIO'.
liftJSM :: MonadJSM m => JSM a -> m a
liftJSM = liftIO

-- | This is the same as 'liftJSM' except when using ghcjs-dom-webkit with GHC (instead of ghcjs-dom-jsaddle)
liftDOM :: MonadDOM m => DOM a -> m a
liftDOM = liftIO

こんな感じで普通のIOになっている。


ghcjs-dom-jsaddleでは

JSDOM.Types as GHCJS.DOM.Types

のようになっていて、その定義は

https://raw.githubusercontent.com/ghcjs/jsaddle-dom/master/src/JSDOM/Types.hs

-- | This is the same as 'JSM' except when using ghcjs-dom-webkit with GHC (instead of ghcjs-dom-jsaddle)
type DOM = JSM
-- | This is the same as 'JSContextRef' except when using ghcjs-dom-webkit with GHC (instead of ghcjs-dom-jsaddle)
type DOMContext = JSContextRef
-- | This is the same as 'MonadJSM' except when using ghcjs-dom-webkit with GHC (instead of ghcjs-dom-jsaddle)
type MonadDOM = MonadJSM

-- | This is the same as 'liftJSM' except when using ghcjs-dom-webkit with GHC (instead of ghcjs-dom-jsaddle)
liftDOM :: MonadDOM m => DOM a -> m a
liftDOM = liftJSM

-- | This is the same as 'askJSM' except when using ghcjs-dom-webkit with GHC (instead of ghcjs-dom-jsaddle)
askDOM :: MonadDOM m => m DOMContext
askDOM = askJSM

-- | This is the same as 'runJSM' except when using ghcjs-dom-webkit with GHC (instead of ghcjs-dom-jsaddle)
runDOM :: MonadIO m => DOM a -> DOMContext -> m a
runDOM = runJSM

全てJSMを使うようになっている。


JSMは
https://github.com/ghcjs/jsaddle/blob/master/jsaddle/src/Language/Javascript/JSaddle/Types.hs
ここで GHCJSの場合はIOにしているけれども、GHCの場合はIOと似ているが異なる型になるようだ。
GHCを使う場合には、JSMとIOは変換できないっぽい。



もしかすると、ghcjs-base-stubのように、ghcjs-dom-stubとかを作っていけば良いのかもしれないが、大量に型があって自分で作るのはつらい。
別の方法を考える。


ーーーー

ここまで書いて、ちょっとサンプルを作った。
github.com
type JSM = IO 版。

自動生成されたコードが何万行もあってつらい…。
正規表現置換で一気にいけるところは変更して、無理なところは手動でぽちぽちやった。
折角なので開発中にJSMをprintしたら分かりやすいメッセージを出すようなスタブが出来れば良かったんだけど、自動生成じゃないと無理そう。普通にjsaddleを使うかね…。
少ししか(と言っても数万行)書いてないのに既にコンパイル重くなってきたしね。



型だけ見るなら

{-# LANGUAGE CPP #-}
module FormSample where

import           Control.Monad
import           Control.Monad.IO.Class            (MonadIO (..))
import           Control.Monad.Trans.Class         (lift)
import           Control.Monad.Trans.Maybe         (MaybeT (..), runMaybeT)
import           GHCJS.DOM
import           GHCJS.DOM.Document
import           GHCJS.DOM.HTMLCollection
import           GHCJS.DOM.Types
import           Language.Javascript.JSaddle.Debug


formEl :: MaybeT JSM Element
formEl = do
  doc <- MaybeT currentDocument
  forms <- getForms doc
  form <- MaybeT $ namedItem forms "form"
  return form

someProcess = undefined

app :: JSM ()
app = do
  mform <- runMaybeT formEl
  case mform of
    Just a  -> someProcess a
    Nothing -> undefined
  return ()


#ifdef MIN_VERSION_ghcjs_dom_jsffi

runForm :: IO ()
runForm = app

#else


runForm :: IO ()
runForm =  runOnAll_ app

#endif

jsaddleのデバッグ用関数(?)runOnALLが使える。実行しても特に何も起こらない。

PostgreSQLでビューを使う時の最適化

ちょっと簡単なサンプルが思い付かないけど。
joinが大量にある巨大なビューから少ない件数のデータを取得する場合。

ビュー単体でselectしたら全てインデックススキャンになるようにしていて

  select *
  from large_large_view
  where large_large_view.post_id in (123, 456,789)

これは高速になるようにしていた。
しかし

select large_view.*
from (
  select post_id
  from fav
  where user_id = ?
) as fav
join lateral (
  select *
  from large_large_view
  where large_large_view.post_id = fav.post_id
) as large_view on true

joinすると遅い。
何故か 結合条件で絞り込む前にビューの深いところでseq scanが走っている。
viewとjoinで遅いというのは都市伝説ではなかった模様。

ビューをバラして一段階層を上げてjoinすれば速いし、ビューにしなくても join (select ...) と入れ子にすると遅かった。
lateral効いてないっぽい。


それで一度は諦めていたんだけど、何故か他の箇所で速く動いているものがあった。
そっちの方はよく見ると

select large_view.*
from (
  select post_id
  from fav
  where user_id = ?
) as fav
join lateral (
  select *
  from large_large_view
  where large_large_view.post_id = fav.post_id
  limit 15 -- <= 制限
) as large_view on true

数件で収まらない可能性があるからlimit付けてた。
そうするとプランがガラッと変わってjoinを使わないときと同等になるようだった。
favの方のインデックスでプランナはfavが数件しか無いことが分かり、large_large_viewも数件しかないことが分かる。とプランナが解釈すると思ってたけどそうではなかったみたい。

GHCJSに再度トライ

stack new myghcjs
cd myghcjs


myghcjs.cabal の 各build-depends に追加

  if impl(ghcjs)
    build-depends: ghcjs-base
  if !impl(ghcjs)
    build-depends: ghcjs-base-stub

js.yaml

resolver: lts-8.11
compiler: ghcjs-0.2.1.9008011_ghc-8.0.2
compiler-check: match-exact
setup-info:
  ghcjs:
    source:
      ghcjs-0.2.1.9008011_ghc-8.0.2:
        url: https://github.com/matchwood/ghcjs-stack-dist/raw/master/ghcjs-0.2.1.9008011.tar.gz
        sha1: a72a5181124baf64bcd0e68a8726e65914473b3b

lts-9.21は動かなかった。というかコンパイルのリンクが永遠に終わらなかった。
Linking template Haskell -- Stuck for 10+ hrs · Issue #14 · matchwood/ghcjs-stack-dist · GitHub

stack build --stack-yaml=js.yaml 

jsが出力されているか確認する。


stack.yaml

resolver: lts-13.24
extra-deps:
  - ghcjs-base-stub-0.2.0.0
  - containers-0.5.11.0

GHCJSのコンパイラを使わないビルドテスト(intero用)

stack build


GitHub - ghcjs/ghcjs-base: base library for GHCJS for JavaScript interaction and marshalling, used by higher level libraries like JSC
ここのサンプルを書いてみる。


{-# LANGUAGE CPP               #-}
{-# LANGUAGE JavaScriptFFI     #-}
{-# LANGUAGE OverloadedStrings #-}

module Main where

import           Lib

import           Data.JSString as S

#ifdef __GHCJS__
foreign import javascript unsafe
  "require('console').log($1)" console_log :: S.JSString -> IO ()
#else
console_log :: S.JSString -> IO ()
console_log = error "GHCJS required to use console_log"
#endif

main :: IO ()
main = do
  console_log "yo"

コンパイルが通るプラグマを探すのでハマった。
JavaScriptFFI はGHCJSでしか効かないので、intero用に#ifdef も必要っぽい。

Add -fdefer-ffi-errors flag (#14227) · Issues · Glasgow Haskell Compiler / GHC · GitLab
ここに書いているように#ifdefで分ける。

.yaml や .cabal は変更なし。
JSとやり取りする例は新しくなっていて

A few examples of Foreign Function Interface · ghcjs/ghcjs Wiki · GitHub
こっちの方は古くて動かないようだ。
古い記事を参考にしてしまうと結構ハマる。

Hasteを試したログ

GHCJSの重さが気になっていたところで、こんな記事を見かけた。
qiita.com


Hasteが良さそうなので試してみる。


Hasteのプロジェクトの作り方は stack new した後に
stackoverflow.com
該当箇所をここの設定に書き換える。

% stack exec haste-boot

...


Installed haste-prim-0.5.4.2
Warning: The package list for 'hackage.haskell.org' does not exist. Run 'cabal
update' to download it.
Resolving dependencies...
haste-cabal.bin: Could not resolve dependencies:
trying: time-1.5.0.1 (user goal)
next goal: deepseq (dependency of time-1.5.0.1)
Dependency tree exhaustively searched.

エラーになった。

再度トライ。

% stack exec haste-boot -- --local

Downloading haste-cabal from GitHub
Downloading Google Closure compiler...
`include' is not a directory

エラー。

% mkdir include
% stack exec haste-boot -- --local
Downloading haste-cabal from GitHub
Downloading Google Closure compiler...
utils/unlit: changeWorkingDirectory: does not exist (No such file or directory)

エラーが素っ気ないのでよく分からないけど、--localでhasteのソースを探しているのか…。

https://github.com/valderman/haste-compiler/issues/395
この辺を見ると解決しているような気がする。

stack.yaml を変更してもう一度トライ。

extra-deps:
- haste-compiler-0.5.5.1
- ghc-simple-0.3
- shellmate-0.2.3
- HTTP-4000.2.23
stack build
stack install haste-compiler
stack exec haste-boot

同じエラーになった。




どうにもならないのでソースから入れる。
github.com

git clone https://github.com/valderman/haste-compiler.git
cd haste-compiler

stack build
stack install
stack exec haste-boot

何の問題もなく入った。



バージョンが0.6.0.0になっていたので、stack.yamlのバージョンを変えたら入るんだろうか。

resolver: lts-6.14
extra-deps:
- haste-compiler-0.6.0.0
- ghc-simple-0.3
- shellmate-0.3.4.2
- shellmate-extras-0.3.4.1
- http-conduit-2.2.4
- http-client-0.5.14
- http-client-tls-0.3.5.3
- tagged-0.8.6


stackoverflow.com
うーん、stack build --copy-compiler-tool haste-compiler は何か違うっぽい。





取り敢えずここまでのソースから入れたあとの設定を最初からやる。

stack new haste-project
cd haste-project

compilerは使わないのでhaste-libだけ書く。
.cabal

  build-depends:
      base >=4.7 && <5
    , haste-lib


stack.yaml

resolver: lts-6.14

packages:
- .

extra-deps:
- haste-lib-0.6.0.0
- binary-0.7.6.1
- containers-0.5.6.3
- haste-prim-0.6.0.0
- time-1.5.0.1

これでstack build出来るし、import Hasteできた。


GHCのバージョンが古くてinteroが動かなくなったので intero.el を修正する。

(defun intero-copy-compiler-tool-auto-install (source-buffer targets buffer)
  "Automatically install Intero appropriately for BUFFER.
Use the given TARGETS, SOURCE-BUFFER and STACK-YAML."
  (let ((ghc-version (intero-ghc-version-raw)))
    (insert
     (format "

Installing intero-%s for GHC %s ...

" intero-package-version ghc-version))
    (redisplay)
    (cl-case
        (let ((default-directory (make-temp-file "intero" t)))
          (intero-call-stack
           nil (current-buffer) t nil "build"
           "--copy-compiler-tool"
           (concat "intero-" intero-package-version)
           "--flag" "haskeline:-terminfo"
           "--resolver" (concat "ghc-" ghc-version)
           ;; "haskeline-0.7.5.0"
           "haskeline-0.7.4.0"
           "ghc-paths-0.1.0.9" "mtl-2.2.2" "network-2.7.0.0" "random-1.1" "syb-0.7"))
      (0
       (message "Installed successfully! Starting Intero in a moment ...")
       (bury-buffer buffer)
       (switch-to-buffer source-buffer)
       (intero-start-process-in-buffer buffer targets source-buffer))

haskeline のバージョンを下げる。
intero自体のバージョンはdefcustomで変更できるようになってるんだけど、この依存ライブラリは固定なので直接編集するしかない。
編集したらbytecompileして起動確認。



初期のファイルを

module Lib
    ( someFunc
    ) where

import Haste

someFunc :: IO ()
-- someFunc = putStrLn "someFunc"
someFunc = writeLog $ toJSString "someFunc"

console.log出力に変えてコンパイル

#!/bin/bash

TARGET=compiled.js

set -eux

mkdir -p build

hastec -O2 -fglasgow-exts \
      --opt-minify --opt-minify-flag='--jscomp_off=checkVars' \
      -o build/compiled.js \
      --outdir=build \
      app/*.hs src/*.hs

24KBになった。
自分でclosure compilerを実行したときより誤差程度軽い。

最初のURLの比較のように3KBには遠い。
バージョンが上がって機能が増えて重くなったんだろうか。


とりあえず一旦ここまで。

GHCJSを使えるようにしたログ

github.com

ソースからのインストールは出来たんだけど、それをstackで使う方法が分からなかった。
エラーがややこしいので元々入っているcabalを消したりhaskell-platformを消したりしてstackだけ動いている状態にして試した。
コンパイルが1時間近くかかる。長い。


取り敢えず動いたのはいいんだけど、emacsで不備が出る。stackで動かしたい。

https://docs.haskellstack.org/en/stable/ghcjs/#ghcjs
ここからのリンクの
https://github.com/matchwood/ghcjs-stack-dist
この設定をコピペして使う。

jsのビルドはスムーズになった。
だがやっぱり普通のHaskellのソースを書くようには行かない。


どうやらemacsのinteroが動かない。
github.com
closeになっているけどghcjs-base-stubもよく分からなかった。ghcjs --interactiveが無理だと厳しい?


よく考えたら素のHaskellが動くし
http://hackage.haskell.org/package/ghcjs-dom
この辺のGHCJS用のライブラリは普通のGHCで動くんだから、普通のGHCコンパイルし直してプログラミング中はHaskellプログラムとしておけば良さそうに思える。


コンパイルコンパイルコンパイル…長い。
github.com
そしてエラーが出る。
jsaddleのコンパイルでメモリ8GB以上持っていかれる。富豪的過ぎる…。
メモリを15GBにしてもスワップも持って行かれた。でも何とかコンパイル成功。

適当にsrcディレクトリをシンクしてGHCJS用のstackファイルとGHC用のstackファイルを用意して、ソースを書くときはGHC用のディレクトリで作業するようにすれば、何とか出来そうだった。



全体的に参考にしたのはこの辺。
qiita.com
labs.spookies.co.jp




それでstackの初期状態 "someFunc" を出力するプログラムをコンパイルしたらjsが1MBある。Closure Compilerで圧縮しても240Kある。
まあHaskellのbaseとかの環境が全部入っているとそうなるのか。Closure Compilerで未使用関数だと判別できない部分が多かったっぽい。


ちなみにClosure Compilerは

java -jar $DIR/compiler.jar \
     --compilation_level=ADVANCED_OPTIMIZATIONS \
     --define 'goog.LOCALE=ja' \
     --define 'goog.DEBUG=false' \
     --jscomp_off=checkVars \
     $GHCJSOUT \
     > $COMPILEDJS

こんな感じで、 --jscomp_off が必要。普通にやると関数の多重定義でエラーになる。

emacs-purpose(window-purpose)でバッファを開くウィンドウを目的(モード、ディレクトリ)ごとに固定する

昨日の続き。
Emacsの使い方を変えようとして無理だった - ログ日記


似たような質問を発見した。
stackoverflow.com
この人は .c と .h でウィンドウを固定したかったみたい。

ここでは buffer-stack が挙げられている。
これも少し使ってみたけれど、まあその名の通りstackなので自分の目的とちょっと違う。操作もちょっと大変そう。

次のコメントでは、.hファイルの次のファイルを開くのはIDEからEmacsにbad idea を持ち込むことだとまで言われているな…。まあ自分の場合はファイル名の頭文字とtabで探すのでちょっと異なるのだが。



Buffer management - WikEmacs
wikiを眺めていると、See AlsoにKeep a certain kind of buffers in a window: Purposeという項目があることに気付いた。separate bufferやbuffer list groupingとかで見ていたのでスルーしていた。

emacs-purpose(window-purpose)で目的ごとにバッファリストの設定が出来るっぽい。
確かに、何のためにウィンドウ別の履歴が欲しいのかと言うと、ウィンドウごとにそれぞれ別の目的があるからで、こちらの方がより上位(?)の概念である。単に履歴を分割するよりはhtmlを編集しているときにC-x C-f でphpを開いたり C-x 0 で閉じたとしても、phpファイルを開いたらphp用の別のウィンドウで立ち上がってくれる方が有り難い。

何故誰もオススメしていないのか…。



Usage · bmag/emacs-purpose Wiki · GitHub
ここの最後にdesctop-save-modeにはデフォルトで対応していると書いている。
なかなか良い感じに思えるが、軽く使ってみると以下の二点で不満が出てきた。

・バッファ名ではなくてファイルのpathでルールを作りたい
・切り替え時にデフォルトを表示してほしい


なのでカスタマイズするelispを書いた。

;; overwrite
(defadvice purpose--buffer-purpose-name-regexp-1 (around path-regexp (buffer-or-name regexp purpose))
  "Add filename with regexp feature."
  (let* (
         (mode (unless (stringp regexp)
                 (car regexp)))
         (mode-match (if mode (eq (purpose--buffer-major-mode buffer-or-name) mode) t))
         (bufname (or (and (bufferp buffer-or-name)
                           (buffer-name buffer-or-name))
                      buffer-or-name)
                  )
         (regexps (if (stringp regexp)
                      regexp
                    (cdr regexp)))
         (target (if mode
                     (buffer-file-name (get-buffer bufname))
                   bufname))
         )

    (when (and mode-match target (string-match-p regexps target))
      (setq ad-return-value purpose)))
  )
(ad-activate 'purpose--buffer-purpose-name-regexp-1)

(defadvice purpose-read-buffers-with-purpose (around with-default (purpose))
  "Prompt with default."
  (let* (
         (other-buffer (delq (current-buffer)
                             (purpose-buffers-with-purpose purpose)))
         (default (or (buffer-name (car other-buffer))
                      "*scratch*"))

         (ret (completing-read
              (concat "[PU] Buffer (default " default "): ")
              (mapcar #'buffer-name
                      other-buffer)
              nil t nil))
         )
    (setq ad-return-value (if (string= ret "") default ret))
    ))
(ad-activate 'purpose-read-buffers-with-purpose)

設定のリストががっつりコアに書かれているので、うまくhookできなかった。なのでコアの関数のカスタマイズ。
引数がregexpじゃなくてコールバックだと良かったんだけれど。

文法を調べつつ書いていてあまり自信なし。
let*で逐次的に書いていてlispっぽくない気がする。


設定はこんなの。

(setq desktop-path '("."))
(desktop-save-mode t)


(require 'window-purpose)
(purpose-mode)

(require 'window-purpose-x)
(purpose-x-kill-setup)

(add-to-list 'purpose-user-mode-purposes '(php-mode . php))
(add-to-list 'purpose-user-mode-purposes '(web-mode . web))
(add-to-list 'purpose-user-mode-purposes '(js2-mode . js2))
(add-to-list 'purpose-user-mode-purposes '(css-mode . css))
(add-to-list 'purpose-user-mode-purposes '(sql-mode . sql))

(add-to-list 'purpose-user-regexp-purposes '((php-mode . "/Page/") . phppage))
(add-to-list 'purpose-user-regexp-purposes '((php-mode . "/Model/") . phpmodel))
(add-to-list 'purpose-user-regexp-purposes '((php-mode . "/Dto/") . phpdto))

(purpose-compile-user-configuration)

;; purpose-switch-buffer-with-purpose to default
(define-purpose-prefix-overload purpose-switch-buffer-overload
  '(purpose-switch-buffer-with-purpose
    switch-to-buffer
    ))


(global-set-key [S-right] 'split-window-horizontally)
(global-set-key [S-left] 'split-window-horizontally)
(global-set-key [S-up] 'split-window-vertically)
;(define-key global-map [S-down] 'delete-other-windows)


(global-set-key [right] 'windmove-right)
(global-set-key [left] 'windmove-left)
(global-set-key [up] 'windmove-up)
(global-set-key [down] 'windmove-down) 

取り敢えずPHPのモデル用のウィンドウ、画面用のウィンドウ、というように指定できるようにした。
major modeとセットなので、ディレクトリに別の拡張子のファイルがあったらそれは別のルールになるようにしている。




本当は .dir-locals.el に書きたかったんだけれど、こっちも書式がよく分からず取り敢えずinit.elに書いた。
やっと設定が出来てきたところなので使い勝手はまた今度。