Help us understand the problem. What is going on with this article?

PureScriptで標準入力

More than 1 year has passed since last update.

TL;DR

はじめに

PureScriptの公式サイトにはPursuitという非常に便利なライブラリ検索エンジンがあります。
例えば、文字列の長さを求める関数を調べたいとしましょう。 strlenだっけlengthだっけsizeだっけ・・・? 何にせよ型は String -> Int なはず。そんなときString->Intをそのまま入れると・・・

https://pursuit.purescript.org/search?q=String+-%3E+Int

はい出てきました! lengthですね!こんな感じで型を入れると、どのライブラリのどの関数かを教えてくれるのです。Haskellで言うところのHoogleみたいな検索エンジンです。

あれ・・・? 無いぞ・・・?

ある日のこと、ある関数を探そうとEffect Stringで検索したんですよ。どんな関数か分かりますか? EffectはHaskellで言うところのIOに相当する型です。そうですね、Haskellで言うところのgetLine :: IO Stringみたいな関数を探していたんですよ。標準入力から一行読み込む関数です。Hoogleでは当然ヒットします。

https://www.haskell.org/hoogle/?hoogle=%3A%3A+IO+String

ですが、Pursuitでは・・・

https://pursuit.purescript.org/search?q=Effect+String

最初にヒットするのは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のドキュメントを見てみましょう。

https://nodejs.org/api/readline.html

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

コード

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

https://github.com/goldarn-ring/how-to-stdin-purescript

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away