LoginSignup
7
3

More than 5 years have passed since last update.

PureScriptで標準入力

Last updated at Posted at 2018-11-25

TL;DR

はじめに

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

コールバックがresolverejectの二つではなく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

最後にreadLinereadLineSyncを使って、名前を聞いてアイサツするコードを書いてみましょう。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

コード

冒頭にも貼りましたが、最後に書いたコードへのリンクを張っておきます。コードはご自由にお使いください。

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3