TL;DR
- PureScriptで標準入力を扱うコードが想像以上にめんどかった
- 書いたコードは https://github.com/goldarn-ring/how-to-stdin-purescript
はじめに
PureScriptの公式サイトにはPursuitという非常に便利なライブラリ検索エンジンがあります。
例えば、文字列の長さを求める関数を調べたいとしましょう。 strlen
だっけlength
だっけsize
だっけ・・・? 何にせよ型は String -> Int
なはず。そんなときString->Int
をそのまま入れると・・・
はい出てきました! length
ですね!こんな感じで型を入れると、どのライブラリのどの関数かを教えてくれるのです。Haskellで言うところのHoogleみたいな検索エンジンです。
あれ・・・? 無いぞ・・・?
ある日のこと、ある関数を探そうとEffect String
で検索したんですよ。どんな関数か分かりますか? Effect
はHaskellで言うところのIO
に相当する型です。そうですね、Haskellで言うところのgetLine :: IO String
みたいな関数を探していたんですよ。標準入力から一行読み込む関数です。Hoogleでは当然ヒットします。
ですが、Pursuitでは・・・
最初にヒットするのはcwd
ですね。名前からしてカレントディレクトリを返す関数でしょう。確かにEffect String
ですね。getLine
とかreadLine
みたいな名前の関数は・・・あれれー・・・? purescript-line-readerというライブラリの readLine :: forall eff m. MonadAff (readline :: READLINE | eff) m => MonadAsk Interface m => m String
という関数は見つかりますが、型を見れば(PureScriptをお使いの方は)お分かりのとおり、PureScript 0.11時代の関数なので、できれば使いたくないやつです。
我らが求める readLine :: Effect String
は・・・。まさか自分で実装するしか無いのか!?
Node.jsを調べよう!
PureScriptはJavaScriptにコンパイルされる言語(いわゆるAltJS)です。Node.jsで標準入力を扱うコードがどんなものか調べれば、大いに参考になるはずです。というわけでNode.jsのドキュメントを見てみましょう。
readlineというライブラリのcreateInterface
関数で入出力インターフェースのオブジェクトを作り、一行入力されたときに呼ばれるコールバックを設定することで入力を読み取ることができるようです。これを使って、名前を聞いてアイサツするコードを書くと以下のようになりました。
const readline = require('readline')
function readLine() {
const reader = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
reader.setPrompt('> ')
reader.prompt()
return new Promise((resolve, reject) => {
reader.on('line', (line) => {
resolve(line)
reader.close()
})
})
}
async function main() {
console.log('Hello. What is your name?')
const name = await readLine()
console.log('Hello, ' + name)
}
main()
このreadLine関数をPureScriptを使って書き直せば・・・
PureScriptで実装しよう!
PureScriptでは、前述のreadlineのバインディングが公式に提供されています。
https://pursuit.purescript.org/packages/purescript-node-readline
(現時点でPursitに上がっているドキュメントは古いので注意。GitHubの方のコードを見たほうがいいかもしれません)
このライブラリを使えば前掲のJavaScriptで標準入力を扱うコードをPureScriptに書き直せます。
というわけで書き直したのが以下のコードです!
import Prelude
import Data.Either (Either(..))
import Node.ReadLine (createConsoleInterface, noCompletion, setPrompt, prompt, setLineHandler, close)
import Effect.Aff (Aff, Canceler, makeAff, launchAff_, effectCanceler)
import Effect (Effect)
readLine :: Aff String
readLine = makeAff handler
where
handler :: (Either Error String -> Effect Unit) -> Effect Canceler
handler next = do
interface <- createConsoleInterface noCompletion
setPrompt "> " 2 interface
prompt interface
setLineHandler interface \str -> do
close interface
next $ Right str
pure $ effectCanceler $ close interface
コールバックがresolve
とreject
の二つではなくEither
を受け取る一つだけとか、おそらくCtrl+Cを押したときに呼ばれるCanceler
を返しているだとか、細かな違いはありますが、やっていることはもとのJavaScriptのコードと概ね同じです。
非同期処理なのでEffect
ではなくAff
を使うことになってしまいましたが、これで標準入力から一行読み込む関数という当初の目的は達成されました。
FFIを使おう!
もっと簡単な方法はないものでしょうか? 実はFFIを使えばもっと簡単にできます!
FFI(Foreign Function Interface)とはJavaScriptで書かれたライブラリをPureScriptから呼び出す仕組みです。もっと簡単に標準入力を扱えるNode.jsのライブラリを見つけて、それをFFIを使って呼び出せばいいのです。
というわけで、このreadline-syncというライブラリを使いましょう。
https://www.npmjs.com/package/readline-sync
このライブラリを使うと、たったこれだけのコードでこれまで書いてきたreadLine
と同じことができるのです。
var readlineSync = require('readline-sync')
exports.readLineSync = function () {
return readlineSync.question('> ')
}
そしてこのreadLineSync
をPureScriptから呼び出せるようにするには、これだけのコードでOKです!
foreign import readLineSync :: Effect String
最後にreadLine
とreadLineSync
を使って、名前を聞いてアイサツするコードを書いてみましょう。readLine
の方はAff
を返すので、使うためにはlaunchAff_
やliftEffect
を使って方を合わせる必要がありますが、readLineSync
の方はその必要もなく素直に呼び出せています。
main :: Effect Unit
main = do
askNameSync
askNameAsync
askNameAsync :: Effect Unit
askNameAsync = launchAff_ do
liftEffect $ log "(Async) Hello. What is your name?"
name <- readLine
liftEffect $ log $ "Hello, " <> name
askNameSync :: Effect Unit
askNameSync = do
log "(Sync) Hello. What is your name?"
name <- readLineSync
log $ "Hello, " <> name
コード
冒頭にも貼りましたが、最後に書いたコードへのリンクを張っておきます。コードはご自由にお使いください。