Lispイベントについて

この記事では、これまでのLispに関するイベントとCommon Lisp Seminar 2019 を振り返りながら、来年度のLispイベントに向けての個人的な考えを書きます。

そもそもLisp研修会をなぜ始めたのかを振り返ると、3年前に小出さんが開催された 人工知能プログラミングのための Common Lisp 入門 が事の始まりでした。天川村の話から、温泉地で勉強会をしましょうかという話になりました。tomabuさんが関西Lispユーザ会の話をされた日もあの日の夜でした。あのイベントがなければ、関西Lispユーザ会の発足からこのような展開になっていなかったと思うとすごく感慨深いです。

1回目のLispイベントは、洞川温泉に小出さんをお招きしてのCommon Lisp研修会でした。旅館と交渉したり、進行案を練ったり、教材を印刷したり、色々しながら、すごくやりがいがありました。夕食、温泉、川辺での花火。その後のcxxxrさんの移住、天川村freaksの収録。研修会自体よりも、研修会を通してLisper同士が繋がっていくのが楽しかったです。

2回目のLispイベントは、関西もくもく.lisp #1と題を打って、Common Lispのもくもく会を開催しました。発表形式に加えて手を動かすイベントをしたいと相談をした後、まずは t-coolとしてイベントを企画することになりました。団体の規約を作って大阪府の公民館で団体利用できるようにしたり、ポケットWi-fiをかりたりしながら、Lispを書いて楽しむ場所と時間を定期的に作れればと思っていました。関西もくもく.lisp #1 の後は、隔月でもくもく会を開催することができて、毎回、myaoさんに助言をもらいながら、少しずつLispを書けるようになってきてるなーと思います。

今年は8月25日(土)に大阪の本町で Survival Common Lispの読書会を開催しました。今年は関西Lispユーザ会の主催としてイベントを開催できることになりました。去年までは1人で準備しなきゃ...と力んでましたが、今年はmyaoさんとtomabuさんが助けてくれたので、当日の準備だけじゃなくて、当日までの気持ちの面でも助かりました。Twitterで myaoさん が、イベントの告知を何度もしていただいていたおかげで、これまで関西Lispユーザ会に参加されたことのない方も数名参加していただきました。合計13名の方に参加していただきました。一番遠い方だと、鳥取から来ていただいた方もいました。

今回のLisp研修会は、Survival Common Lispの読書会として企画を提案しました。結局蓋を開けて見ると、大半がCommon Lispの経験者の方々で、疑問点が出てきたときには自己解決ができる方々ばかりでした。書籍の中での修正箇所をPRしていただいたり、テストでの文字コードでのお話などの指摘が入ったりと、編集過程で書籍を読み直すような作業に近かったように思います。

事前にSurvival Common Lispを各自で読み進めてもらい、当日のイベントで質疑応答をしていただく形で進めました。当初は、時間で区切って内容を変えながら、全員で同じ章を読み進めるようにスケジュールを組んでいました。著者の4名が参加してもらえることになったので、テーブルを4つに分けて著者の4名に別れて座っていただき、質問があれば、直接、著者の方々の所に行って質問をしてもらう形にしました。

今回の参加者の大半がCommon Lispの経験者だったので、自分で問題なく読み進められて質問がない状態が長く続いたように思いました。経験者の皆さんは各自で作業を進められていたようでした。参加者の大半が経験者の中、入門書を読み進めるという目的が、ニーズとマッチしていないのではと途中で考えていました。今デスクによっては書籍と関係ない話で盛り上がったときもあって、「これ、なんの会っすか?」という声も聞こえたので、進め方について運営でじっくりと議論をしておいたほうがよかったかなーと思いました。

参加者の方々から環境設定やエラーの解決について質問がでたときに、snmstsさんが1件ずつ丁寧に助言されながら問題を解決されていく姿を見て、これが問題解決ってことかーとプロの作法を知りました。最初に僕が エラーを見て「Jonathan インストールできてますか?」と見当違いのことを言ってしまったのが恥ずかしかったです。エラーメッセージを性格に読むには、経験と洞察が必要だと思いました。

fukamachiさんに進行の詳細を聞かれたときに、仲間から「進行の詳細なんて考えてる訳ないじゃないですか。Lispといえば人が集まると思っているんですよ」の一言はグサッときました。事前に十分に内容や進め方について議論できていれば、そういう声は出なかったと思うので、イベントを提案した本人として、真摯に反省して、次のイベントに向けて頑張ろうと思います。

「myaoさんと来年のLisp合宿を企画しようと話をしています。」という話をfukamachi さんと masatoi さんに伝えて、どういう形が良いか、アドバイスをもらいました。数人のグループでハッカソン形式で共同開発を進めて、最終日に成果物を共有する。事前にグループを決めておいて、グループごとに2泊3日で実現可能なテーマを決めておき、2泊3日のハッカソンに取り組むのはですか?との助言をもらいました。個人がアプリケーションの提案書を出して、そのアイデアに興味をもった人で一緒に開発をするのがいいのかなーと思っています。僕は英語学習システムの開発を企画書として出そうと思います。

と、これまでの反省しながら、来年度に向けての展望でまとめました。来年、今日来ていただいた方々とまた会える日を楽しみにしながら、日々の生活に精進していこうと思います。

Reactを用いたクイズアプリ開発


Mitch Gavan氏による記事 Create a quiz with Reactの日本語訳です。

著者から許可をいただき、記事の日本語訳を公開しています。


react-quiz.jpg

本記事では、Reactを用いてクイズアプリを作成します。Facebookによる Create React App を使うため、ビルドに関する環境設定を行う必要がありません。Reactのプロジェクトを作成するには多くの手順が必要ですが、 Create React App を使うと、Webpack、Babel、ESLint等の最新のワークフロー構成でプロジェクトを作成することができるため、Reactを用いた開発をすぐに始めることができます。本チュートリアルを完了後のクイズアプリは こちら から確認できます。

初期設定

バージョン4以上のNode.jsがインストールされていることを確認してください。任意のディレクトリーで次のようにコマンドを打ち込み、Reactを用いたアプリケーションの雛形を作成します。

$ npx create-react-app react-quiz
$ cd react-quiz

ここでは react-quiz と名前をつけましたが、プロジェクト名は何でも構いません。プロジェクトを作成すると、 react-quiz という名前で新しくフォルダが作成されます。フォルダ内にはプロジェクトの雛形が生成されて、依存ライブラリーがnode_modules内にインストールされます。プロジェクトは次のような構成になります。

react-quiz/
  README.md
  index.html
  favicon.ico
  node_modules/
  package.json
  src/
    App.css
    App.js
    index.css
    index.js
    logo.svg

インストール完了後、次のようにアプリケーションを起動することができます。

$ npm start

ブラウザで http://localhost:3000 を開くと、アプリケーションを試すことができます。雛形のコードに慣れるために、プロジェクト内のコードをじっくり見てください。プロジェクト内のコードを変更すると、自動でアプリケーションがリロードされます。もしビルドエラーやリントの警告があった場合、コンソール上で確認することができます。最新の開発環境ができました。では、クイズアプリの開発を始めましょう。

作成するアプリケーションについて

作成するクイズは、質問は複数の選択肢を持ち、どの選択肢を選ぶかによって異なる結果が表示されるアプリケーションです。今回扱うデータは、3つのゲーム開発会社(任天堂、Sony、Microsoft)のうち、どの会社のゲームが一番好きかを判定するためのものです。このクイズでユーザーは、5問の質問のなかで3つの選択肢のうち1つを選択します。質問と答えの数は増やすことが可能です。

スクリーンショット 2019-08-05 21.35.48.png

スクリーンショット 2019-08-05 21.36.45.png

アプリケーションを構築するにあたり、Facebookが提唱する Thinking in React で示される手法で開発を進めます。この手法では、小さなコンポーネントを組み合わせながらアプリケーションを構築します。このクイズアプリでは、次の4つのコンポーネントを定義します。

  • Question (質問)
  • Question count (質問のカウント)
  • Answer options (回答の選択肢)
  • Result (結果)

これらのコンポーネントは、クイズアプリをビルドする際、Containerコンポーネントで組み合わされます。

1つ目のコンポーネントを作成する

まず、プロジェクトルートで次のコマンド実行して、prop-typesをインストールします。

$ npm install prop-types

次に、 components という名前のディレクトリを作成して、その中に Question.js という名前で新しくファイルを作成します。ここに1つ目のReactコンポーネントを書きます。次のようにファイルを編集してください。

import React from 'react';
import PropTypes from 'prop-types';

function Question(props) {
  return (
    <h2 className="question">{props.content}</h2>
  );
}

Question.propTypes = {
  content: PropTypes.string.isRequired
};

export default Question;

なぜクラスを使わずにコンポーネントを作成したか疑問に思った方もいるでしょう。クラスを使わないのは、このコンポーネントが stateをもたないpresentationコンポーネント であるからです。ここではクラスを使わないことにより、クラスに由来する多くの記述を省くことができます。

Reactでは、コンポーネントを2つの種類(presentationalcontainer )に分類する慣習があります。物事の動作に関わるものは container コンポーネント、見た目に関わるものは presentational コンポーネントと分類するのが基本的な考え方です。詳しくはこの記事を参照してください。

ここで定義した Question コンポーネントは、質問を表示するだけのものです。質問の内容は、container コンポーネントから propsを通して渡されます。 Reactにおいて propTypes (property typesの省略形) は、開発者を手助けするために使われます。 propTypes は prop の型と、どの props が必要かをを定義します。

次のようにimport文を書き、Questionコンポーネントを mainコンポーネント (App.js) に追加します。

import Question from './components/Question';

App コンポーネントのrender関数で Questionコンポーネントを使います。

render (
  <div className="App">
    <div className="App-header">
      <img src={logo} className="App-logo" alt="logo" />
      <h2>React Quiz</h2>
    </div>
    <Question content="What is your favourite food?" />
  </div>
);

ここでは、コンポーネントのテストのために、文字列を propsのcontentに渡しています。この箇所は後に変更します。もしアプリケーションを試しに起動すると、質問が What is your favourite food? として表示されるはずです。

他のコンポーネントを作成する

次に、QuestionCountコンポーネントを作成します。 components フォルダの中に、 QuestionCount.js という名前でファイルを作成して、次のように編集します。

import React from 'react';
import PropTypes from 'prop-types';

function QuestionCount(props) {
  return (
    <div className="questionCount">
      Question <span>{props.counter}</span> of <span>{props.total}</span>
    </div>
  );
}

QuestionCount.propTypes = {
  counter: PropTypes.number.isRequired,
  total: PropTypes.number.isRequired
};

export default QuestionCount;

QuestionCountコンポーネントは、先ほどのQuestionコンポーネントと似ています。containerコンポーネントから、2つのprops(countertotal)を受け取ります。

次に、回答の選択肢を表示するために AnswerOptionコンポーネントを作成します。 components フォルダの中に、 AnswerOption.js という名前でファイルを作り、以下のように編集します。

import React from 'react';
import PropTypes from 'prop-types';

function AnswerOption(props) {
  return (
    <li className="answerOption">
      <input
        type="radio"
        className="radioCustomButton"
        name="radioGroup"
        checked={props.answerType === props.answer}
        id={props.answerType}
        value={props.answerType}
        disabled={props.answer}
        onChange={props.onAnswerSelected}
      />
      <label className="radioCustomLabel" htmlFor={props.answerType}>
        {props.answerContent}
      </label>
    </li>
  );
}

AnswerOption.propTypes = {
  answerType: PropTypes.string.isRequired,
  answerContent: PropTypes.string.isRequired,
  answer: PropTypes.string.isRequired,
  onAnswerSelected: PropTypes.func.isRequired
};

export default AnswerOption;

AnswerOptionコンポーネントは、ラジオボタンとラベルをもつリストとして構成されます。11行目のchecked プロパティの値に比較演算子が含まれる点に注目してください。この値は、回答として選んだ選択肢が、回答の選択肢の型に合うかどうかにより、true か falseが設定されます。

コンポーネントを組み合わせる

ここまでに作成した3つのコンポーネント(Question, QuestionCount, AnswerOption)を Quizコンポーネントとして結合させましょう。components フォルダの中に、 Quiz.js という名前でファイルを作ります。

まず、import文を用いて作成したコンポーネントを取り込みます。

import React from 'react';
import PropTypes from 'prop-types';
import Question from '../components/Question';
import QuestionCount from '../components/QuestionCount';
import AnswerOption from '../components/AnswerOption';

では、Quizコンポーネントを定義しましょう。

function Quiz(props) {
  return (
      <div className="quiz">
        <QuestionCount
          counter={props.questionId}
          total={props.questionTotal}
        />
        <Question content={props.question} />
        <ul className="answerOptions">
          {props.answerOptions.map(renderAnswerOptions)}
        </ul>
      </div>
  );
}

Quiz.propTypes = {
  answer: PropTypes.string.isRequired,
  answerOptions: PropTypes.array.isRequired,
  counter: PropTypes.number.isRequired,
  question: PropTypes.string.isRequired,
  questionId: PropTypes.number.isRequired,
  questionTotal: PropTypes.number.isRequired,
  onAnswerSelected: PropTypes.func.isRequired
};

export default Quiz;

ここでは、以前に作成したコンポーネントを用いて、Quizコンポーネントを組み立て、必要なpropsを渡しています。3つのコンポーネント(Question, QuestionCount, AnswerOption)に渡されていたpropsが、Quizコンポーネントのpropsでも渡されています。なので、Quizコンポーネントも同様にpresentationalコンポーネントとして扱います。表示に関わるコードは、アプリの機能性(functionality)から分離するようにしましょう。

AnswerOptionsの各々を作成するためには、renderAnswerOptions 関数を定義する必要があります。return式の直前に、次のようにコードを書いてください。

function renderAnswerOptions(key) {
  return (
    <AnswerOption
      key={key.content}
      answerContent={key.content}
      answerType={key.type}
      answer={props.answer}
      questionId={props.questionId}
      onAnswerSelected={props.onAnswerSelected}
    />
  );
}

AnswerOptionのプロパティは、後にmainのcontainerコンポーネント (App.js)にて定義されるので心配する必要はありません。ここでは、stateで定義されている回答の選択肢に合わせて AnswerOption コンポーネントがrenderされます。

スタイルを追加する

Create React App では、モジュールごとにCSSファイルを定義できるようにWebpackが設定されます。各々CSSファイルを変更するたびに、全てのCSSファイルが1つにまとめられます。このクイズアプリではスタイルにはこだわらないので、1つのCSSファイルにスタイルを記述します。こちらからコードをコピーして、 index.css の内容を書き換えてください。

機能を追加する

クイズの機能を作成する前に、アプリケーションのstateを定義する必要があります。 App.js のクラスのconstructor関数で起動時のstateを定義します。これはES6で初期状態を定義するときによく使われる手法です。 App クラスを次のように編集します。

constructor(props) {
  super(props);

  this.state = {
    counter: 0,
    questionId: 1,
    question: '',
    answerOptions: [],
    answer: '',
    answersCount: {},
    result: ''
  };
}

stateは、コンポーネントのイベントハンドラーがUIを更新するために変更するデータを含む必要があります。上のコードでは、クイズアプリで必要な全てのstateを含んでいます。では、実際にstateにデータを入れて試してみましょう。apiという名前のフォルダの中にquizQuestions.js というファイルを作成して、こちらのコードをコピーして貼り付けてください。 App.js にインポートします。

import quizQuestions from './api/quizQuestions';

次に、Reactの標準機能の componentDidMountライフサイクルを用いてアプリケーションのstateにデータを注入します。次のコードをconstructorの直下に配置してください。

componentDidMount() {
  const shuffledAnswerOptions = quizQuestions.map((question) => this.shuffleArray(question.answers));  

  this.setState({
    question: quizQuestions[0].question,
    answerOptions: shuffledAnswerOptions[0]
  });
}

コンポーネントがマウントされるとすぐに componentDidMount ライフサイクルのイベントが発動します。componentDidMount メソッド内で setState を呼び出すと、stateが更新されるたびにrender()が一度だけ実行されます。

shuffleArray という関数は、回答の選択肢をランダムにシャッフルするために使います。componentDidMount の下で次のように定義します。

shuffleArray(array) {
  var currentIndex = array.length, temporaryValue, randomIndex;

  // While there remain elements to shuffle...
  while (0 !== currentIndex) {

    // Pick a remaining element...
    randomIndex = Math.floor(Math.random() * currentIndex);
    currentIndex -= 1;

    // And swap it with the current element.
    temporaryValue = array[currentIndex];
    array[currentIndex] = array[randomIndex];
    array[randomIndex] = temporaryValue;
  }

  return array;
};

この関数は配列をシャッフルするためのものです。ここでは実装方法について詳しく取り上げませんが、興味がある方はこちらを参考にしてください。

では、 App コンポーネントのための render関数を定義しましょう。

render() {
  return (
    <div className="App">
      <div className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <h2>React Quiz</h2>
      </div>
      <Quiz
        answer={this.state.answer}
        answerOptions={this.state.answerOptions}
        questionId={this.state.questionId}
        question={this.state.question}
        questionTotal={quizQuestions.length}
        onAnswerSelected={this.handleAnswerSelected}
      />
    </div>
  )
}

render関数の中でイベントハンドラーを束縛している点に注目してください。パフォーマンス上の理由で、イベントハンドラーはconstructorに置くのが最善策です。次のコードをconstructor関数の末尾に加えてください。

this.handleAnswerSelected = this.handleAnswerSelected.bind(this);

stateを更新する

では、回答を選択する機能を追加しましょう。次の関数をrender()関数の上に加えてください。

handleAnswerSelected(event) {
  this.setUserAnswer(event.currentTarget.value);
  if (this.state.questionId < quizQuestions.length) {
      setTimeout(() => this.setNextQuestion(), 300);
    } else {
      // do nothing for now
    }
}

この関数では、setUserAnswerが回答のセット、setNextQuestionが次問のセットをしています。では、setUserAnswer 関数をhandleAnswerSelected関数の前に書きましょう。

setUserAnswer(answer) {
  this.setState((state) => ({
    answersCount: {
      ...state.answersCount,
      [answer]: (state.answersCount[answer] || 0) + 1
    },
    answer: answer
  }));
}

setUserAnswer 関数の実装についてみていきましょう。ユーザの選択に基づいて答えをセットします。これは、アプリ内でユーザがstateを変える初めての場面です。1行目で引数のanswerに渡される値は、ユーザが選択する回答です。ここでは Nintendo、Microsoft、Sony のいずれかになります。

2行目では関数を引数にして setState を呼び出しています。1つの状態は第1引数として渡されて、1つ前のstateにアクセスすることができます。 setState は、イベントハンドラーとサーバからのリクエストを受けてUIの更新をトリガーするために使われる組み込みのメソッドです。Reactでは、 state は不変のもの(イミュータブル)として扱う必要があります。このような理由で、3行目では新しいオブジェクトを作成しています。このオブジェクトは、新たなanswerCount の値と共に this.state.answersCount の元のプロパティを保持します。(スプレッド構文が使われています。) こうすることで、直接的にstateを変更せずに、stateを更新することができます。

次にsetNextQuestion 関数を定義します。この関数は、stateを更新して、次の質問を表示させます。updatedAnswersCount 関数の下に次のコードを加えてください。

setNextQuestion() {
  const counter = this.state.counter + 1;
  const questionId = this.state.questionId + 1;
  this.setState({
    counter: counter,
    questionId: questionId,
    question: quizQuestions[counter].question,
    answerOptions: quizQuestions[counter].answers,
    answer: ''
  });
}

ここでは、counterquestionId の2つの変数を作成後に setState を経由してそれぞれのstateをインクリメントしています。さらに、questionanswerOption をcounter変数を元に更新しています。これで関数型のアプリケーションが完成しました。あなたが回答を選ぶと、stateが更新されて、次の質問が表示されるはずです。

結果を計算する

結果を計算するために、まずは handleAnswerSelected 関数を更新する必要があります。先ほど空欄だった else文の処理を次のように更新します。

handleAnswerSelected(event) {
  this.setUserAnswer(event.currentTarget.value);
  if (this.state.questionId < quizQuestions.length) {
      setTimeout(() => this.setNextQuestion(), 300);
    } else {
      setTimeout(() => this.setResults(this.getResults()), 300); // この行を追加
    }
}

ここではsetResults を0.3秒後に呼び出しています。この遅延は、ユーザが選択した回答を確認するために設定しています。選ばれた回答の結果はgetResults関数の中で渡されます。では、getResults関数を定義しましょう。

getResults() {
  const answersCount = this.state.answersCount;
  const answersCountKeys = Object.keys(answersCount);
  const answersCountValues = answersCountKeys.map((key) => answersCount[key]);
  const maxAnswerCount = Math.max.apply(null, answersCountValues);

  return answersCountKeys.filter((key) => answersCount[key] === maxAnswerCount);
}

getResults関数は、回答が3つのタイプ(この場合は、Sony、Microsoft、Nintendoのうちいずれか)のうちどれが一番大きな数値だかを計算します。この関数定義はES6のシンタックスで定義されていますが、ES5で定義するより回りくどうように見えます。3行目では、answersCountKeys はオブジェクトの全てのプロパティを表現する文字列の配列を返すために Object.keys を利用しています。

['nintendo', 'microsoft', 'sony']

4行目では、 answersCountValues が値の配列を返すために、この配列にマッピングをしています。その後、Math.max.applyでその配列で一番大きい数値の値をえて。5行目のmaxAnswerCount 変数に割り当てられます。最後に7行目で、filterメソッドを用いて、どのキーがmaxAnswerCount` と等しい値を持つかを計算して返します。

次は setResults 関数をgetResults の下に作成します。

setResults (result) {
  if (result.length === 1) {
    this.setState({ result: result[0] });
  } else {
    this.setState({ result: 'Undetermined' });
  }
}

この関数は getResults から配列の形式で結果を受け取り、その配列が1つの値を持つかを確認します。もし1つの値があるとき、setStateを通して値を設定します。もし配列が1つ以外の値をもつときは、結論が出なかった場合です。その場合は、結果をUndeterminedとします。

結果を表示する

ついに結果を表示するところまできました。 components フォルダーの中にResult.js というファイルを作成します。

import React from 'react';
import PropTypes from 'prop-types';

function Result(props) {
  return (
    <div className="result">
      You prefer <strong>{props.quizResult}</strong>!
    </div>
  );
}

Result.propTypes = {
  quizResult: PropTypes.string.isRequired,
};

export default Result;

Resultコンポーネントは、結果を表示するpresentationコンポーネントです。次に、App.js のrender関数を更新する必要があります。render関数のQuizコンポーネントを次のように更新します。

{this.state.result ? this.renderResult() : this.renderQuiz()}

ここでは、クイズか結果のどちらを表示するかを判断するために、JSXの三項演算子を用いて書きます。もし state.result に値が設定されている場合は結果を表示します。

最後に、これらの2つの関数を作成する必要があります。以下のコードをrender関数の直前に加えます。

renderQuiz() {
  return (
    <Quiz
      answer={this.state.answer}
      answerOptions={this.state.answerOptions}
      questionId={this.state.questionId}
      question={this.state.question}
      questionTotal={quizQuestions.length}
      onAnswerSelected={this.handleAnswerSelected}
    />
  );
}

renderResult() {
  return (
    <Result quizResult={this.state.result} />
  );
}

これで関数型のクイズアプリの完成です!もしアプリをデプロイする場合は、npm run build コマンドで最適化されたビルドを作成してください。最小化されてアプリケーションが書き出されて、デプロイすることができます。

付録: アニメーションを追加する

ユーザの印象を良くするために、アニメーションを少し追加しましょう。Reactのアニメーションコンポーネントをインストールします。ここではバージョン1を利用しています。バージョン2では少し変更があるようです。

npm install react-transition-group@1.x

このプラグインを使うと、ReactのコンポーネントにCSSを用いて簡単にトランジションやアニメーションを加えることができます。もしAngularを用いていると慣れているかと思いますが、Angularの ng-animate ライブラリに影響を受けているようです。質問に対してフェードインとフェードアウトの効果を追加しています。

Quiz.js を開き、次のimport文を追加しましょう。

import { CSSTransitionGroup } from 'react-transition-group';

CSSTransitionGroup はアニメーションに関するコンポーネントをラップしているシンプルな要素です。全てのクイズコンポーネントにアニメーションを追加することができます。そうするためには、次のようにrender関数を更新します。

return (
  <CSSTransitionGroup
    className="container"
    component="div"
    transitionName="fade"
    transitionEnterTimeout={800}
    transitionLeaveTimeout={500}
    transitionAppear
    transitionAppearTimeout={500}
  >
    <div key={props.questionId}>
      <QuestionCount
        counter={props.questionId}
        total={props.questionTotal}
      />
      <Question content={props.question} />
      <ul className="answerOptions">
        {props.answerOptions.map(renderAnswerOptions)}
      </ul>
    </div>
  </CSSTransitionGroup>
);

ここでは CSSTransitionGroup 要素の中で、クイズの要素をラップします。CSSTransitionGroup の子要素は固有のkey 属性を提供される必要があります。このようにして、Reactはどの子要素がenter、left、stayedの状態かを判定します。11行目で props.questionId としてキーを定義して、その値が各々の質問で異なる値になります。

CSSTransitionGroup 要素がもつ多くのプロパティについて見ていきましょう。component プロパティでは、このコンポーネントがどのHTML要素としてレンダーされるかを特定します。 transitionName プロパティは、要素に加えられるCSS要素の名前を特定します。ここでは、要素がレンダーされるときはfade-enterfade-enter-activeが特定され、要素が削除されるときはfade-leavefade-leave-activeが特定されます。transitionEnterTimeouttransitionLeaveTimeoutは、アニメーションの長さを特定しますが、これはCSSの中でも指定する必要があります。必要とされるCSSが index.css で読み込まれていることに気が付いた方もいるでしょうindex.css では次のように記述します。

.fade-enter {
  opacity: 0;
}

.fade-enter.fade-enter-active {
  opacity: 1;
  transition: opacity 0.5s ease-in-out 0.3s;
}

.fade-leave {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  opacity: 1;
}

.fade-leave.fade-leave-active {
  opacity: 0;
  transition: opacity 0.5s ease-in-out;
}

このCSSでは、透過性の値を変更して、トランジションの長さとタイプを特定しています。 CSSトランジションの.fade-enterに0.3秒のディレイを加えるために、transitionEnterTimeout は0.8秒に指定しています。 transitionAppear プロパティは、最初にマウントされるときに、どのコンポーネントがアニメーションされるかを特定するためのものです。transitionAppearTimeout では、アニメーションの長さを指定しています。ここでのCSSは、他のアニメーションにおいても同じように指定されます。

.fade-appear {
  opacity: 0;
}

.fade-appear.fade-appear-active {
  opacity: 1;
  transition: opacity 0.5s ease-in-out;
}

最後に、Result.jsのrender関数を次のように変更する必要があります。

return (
  <CSSTransitionGroup
    className="container result"
    component="div"
    transitionName="fade"
    transitionEnterTimeout={800}
    transitionLeaveTimeout={500}
    transitionAppear
    transitionAppearTimeout={500}
  >
    <div>
      You prefer <strong>{props.quizResult}</strong>!
    </div>
  </CSSTransitionGroup>
);

これにより、Resultコンポーネントが確かにアニメーションされるようになります。これでアニメーションの機能は完成です!

クイズアプリの完成

このGithubのレポジトリでソースコードを確認することができます。このチュートリアルがあなたの役にたつことを望んでいます。多くの概念をすごく手短に説明した。もし質問がある場合は、Twitterのアカウント にコンタクトをとってください。

著作権

©️ Mitch Gavan 2019

Utopian手習い #01

Utopian手習い

本記事では、Common LispのWebフレームワークであるUtopianの最新バージョンnextを紹介します。nextブランチにあるデモ用のブログに機能を追加しながら、Webアプリケーションを開発します。

目 次

 1. 準備

 2. 閲覧ページの実装

 3. 投稿/編集ページの実装

 4. デザインの変更(cl-bootstrap)

 5. Login/Logoutの実装

 6. Following/Followedの実装

1. 準備

Common Lispの環境構築にはroswell、エディタにはlemを使います。

LinuxかmacOSにroswellとlemをインストール後、読み進めてください。

まずは、Utopianをインストールしましょう。

ros install fukamachi/utopian

Utopianのsrcディレクトリに移動して、nextブランチに切り替えます。

cd ~/.roswell/local-projects/fukamachi/utopian/
git checkout next

以上で、準備は完了です。

  1. 閲覧ページの実装

ブログのファイル構成

では、ブログの開発を始めましょう。まず、ブログのディレクトリに移動します。

cd ~/.roswell/local-projects/fukamachi/utopian/example/

ソースファイル群の構成を確認しましょう。

$ tree
.
├── Makefile
├── app.lisp
├── config
│   └── environments
│       └── local.lisp
├── db
│   ├── migrations
│   ├── myblog.db
│   └── schema.sql
├── models.lisp
├── myblog.asd
├── routes.lisp
└── views.lisp

makeコマンド用にMakefile、システムファイルとしてmyblog.asd、全体用にapp.lisp、モデル用にmodels.lisp、ビュー用にviews.lisp、ルーティング用にroutes.lisp、環境設定関連はconfigフォルダにファイルが配置されています。

では、Makefileから見ていきます。

all: server

server:
    ros -s myblog -e '(utopian/tasks:server myblog/app:blog-app)'

generate-migrations:
    ros -s myblog -e '(utopian/tasks:generate-migrations myblog/app:blog-app)'

migrate:
    ros -s myblog -e '(utopian/tasks:migrate myblog/app:blog-app)'

make serverでアプリケーションの起動、make generate-migrationsでマイグレーション・スクリプトの生成、make generate-migrateでマイグレーションを実行してデータベースを更新します。

アプリケーションの起動

では、make serverコマンドで、アプリケーションを起動しましょう。

$ make server
ros -s myblog -e '(utopian/tasks:server myblog/app:blog-app)'
Hunchentoot server is going to start.
Listening on localhost:5000.

ブラウザを開いてhttp://localhost:5000にアクセスすると、次のように、初期画面が確認できます。

f:id:tcooooooool:20180914201619p:plain

f:id:tcooooooool:20180914201625p:plain

モデルの更新

では、models.lispを編集して、本文(content)を追加できるようにしましょう。

$ lem models.lisp

(defpackage #:myblog/models
  (:use #:cl
        #:utopian)
  (:export #:entry
           #:entry-title
       #:entry-content))  ; <= entry-contentでDBにアクセスできるように、シンボルをエクスポートする。
(in-package #:myblog/models)

(defmodel entry ()
  ((title :col-type :text)
   (content :col-type (or :text :null); <= entry-contentでDBにアクセスできるように、シンボルをエクスポートする。
            :initform "BODY")))

(defmodel entryのS式が、記事用のモデルです。

defmodelは、defclassの形式に沿って定義されています。

title(記事タイトル)とcontent(本文)のカラム型には文字列を指定しています。

  ((title :col-type :text)
   (content :col-type (or :text :null)
            :initform "BODY"))

マイグレーション

モデルを変更したあとは、make generate-migrationsmake migrateを実行して、データベースを更新します。

make generate-migrations
make migrate

ビューの更新

ユーザが操作をする画面(ビュー)に本文が追加できるように、views.lispを編集します。

entryのbodyの要素に(p (entry-body entry))を追加することで、本文を挿入できるようになります。

$ lem views.lisp

(defview entry ()
  (entry)  ; <== entryモデルからDBの情報をとってきます
  (:render
   (html
    (head
     (title (format nil "~A | Myblog" (entry-title entry))))
    (body
     (h1 (entry-title entry))
     (p "Entry page!")
     (p (entry-body entry))))))  ; <== この行を追加します。

ルーティングの更新

routes.lispで、ルーティングを変更します。

$ lem routes.lisp

(defpackage #:myblog/routes
  (:use #:cl
        #:utopian
        #:myblog/views
        #:myblog/models)
  (:import-from #:assoc-utils
                #:aget)
  (:export #:index
           #:entries
           #:entry))
(in-package #:myblog/routes)

;; トップ画面 (http://localhost:5000)
(defroute index ()
  (render))

;; 記事の一覧を表示する (http://localhost:5000/entries)
(defroute entries ()
  (render :entries (mito:select-dao 'entry)))

;; 個別に記事を表示する (http://localhost:5000/entry/記事の番号)
(defroute entry (params)
  (render :entry (mito:find-dao 'entry :id (aget params :id))))

app.lispの更新

app.lispで、アプリケーション全体の設定をします。

$ lem app.lisp

(defpackage #:myblog/app
  (:use #:cl
        #:utopian
        #:myblog/routes)
  (:export #:blog-app))
(in-package #:myblog/app)

(defapp blog-app
  ((:GET "/" #'index) ; http://localhost:5000/ へのリクエストを、index Viewに渡す。
   (:GET "/entries" #'entries) ; http://localhost:5000/entries へのリクエストを、entries Viewに渡す。
   (:GET "/entries/:id" #'entry)) ; http://localhost:5000/entries/記事の番号へのリクエストを、entry Viewに渡す。
   
   (:config #P"config/environments/"))

開発環境の切り替え

config/environments/local.lispで、開発段階の設定をします。今回の開発段階では、SQLite3を使います。

lem config/environments/local.lisp
(defpackage #:myblog/config/environments/local
  (:use #:cl))
(in-package #:myblog/config/environments/local)

'(:database (:sqlite3
             :database-name #P"db/myblog.db"))

DBの更新

今回は、まだ投稿用のコマンドを実装していないので、DB Browser for SQLite等のDBエディタでdb/myblog.dbを編集します。

f:id:tcooooooool:20180914201628p:plain

ページの確認

最後に、ページの変更を確認しましょう。

f:id:tcooooooool:20180914201632p:plain

今回は、これで終わりです。次回は、新しく記事を追加する機能を実装していきます。 f:id:tcooooooool:20180914201619p:plainf:id:tcooooooool:20180914201625p:plainf:id:tcooooooool:20180914201628p:plainf:id:tcooooooool:20180914201632p:plain

package-inferred-systemでモダンなLispライブラリを書く方法

この記事は、David Vázquezさんの記事 [How to write a modern Lisp library with ASDF3 and Package Inferred System] (http://davazp.net/2014/11/26/modern-library-with-asdf-and-package-inferred-system.html)の日本語訳です。Davidさんに許可をいただき、日本語訳を共有させていただけることになりました。

はじめに

最近よく見かけるパッケージの使い方は、package.lispかpackages.lispを加える手法です。多くの場合、このファイル(package.lispかpackages.lisp)は、最初に読み込まれるファイルで、他のファイルが使うパッケージを定義します。この手法は、多くのプロジェクトで用いられています。しかし、パッケージ群が大きくなるにつれて、複数のファイル間での依存関係を管理する規則が必要になります。

パッケージ群を定義するための代替策として、1ファイル1パッケージ(one package per file)と名づけられた手法があります。その名前が示すように、この手法では、全てのファイルはdefpackageで始まります。複数のパッケージ間の依存関係が明示的で、全てのファイルが固有のパッケージ名をもつので、ファイル間の依存関係は推測(inferred)されます。

この手法は数年前にfaslpathとquick-buildによって導入されました。しかし近年、事実上の標準Common LispビルドシステムであるASDF3が、asdf-package-systemの拡張でこの手法をサポートしました。その結果、今日、この手法を使うことはより簡単になりました。

では、少しの間、記事を一緒に読み進めながら、あなたのプロジェクトで、この手法を使う手法を学びましょう。お役に立てれば嬉しいです。

使い方

まず初めに、asdf-package-system拡張を、あなたのシステムで有効にしなければいけません。projectという名前でシステムを定義することから始めましょう。

(asdf:defsystem :project
  :name "My Project"
  :version "0.0.1"
  :class :package-inferred-system
  :defsystem-depends-on (:asdf-package-system)
  :depends-on (:project/addons))

ここでは、システム、パッケージ、ファイルの間での一連の流れを定義しています。project/foo/barというシステムは、foo/bar.lispを参照します。foo/bar.lispは、project/foo/barパッケージを提供しなければいけません。useされてimportされるproject/foo/barパッケージは、同じ名前のシステムを参照します。

例えば、projectというシステムは、project/addonsに依存しているので、addons.lispが先に読み込まれなければいけません。addons.lispの内容は、次のように始まります。

(defpackage :project/addons
  (:use :common-lisp :project/core)
  (:import-from :cl-ppcre))

(in-package :project/addons)

(cl-ppcre:scan-to-strings "h.*o" (hello))

project/coreパッケージをuseしていて、cl-ppcreパッケージを”import”していることに注意してください。よって、ASDFは、それがcl-ppcreとproject/coreの両システムに依存していると推測します。project/coreシステムは、core.lispを参照することを覚えておいてください。core.lispの内容は、次の通りです。

(defpackage :project/core
  (:use :common-lisp)
  (:export #:hello))

(in-package :project/core)

(defun hello ()
  "Hello!")

たったこれだけです。このファイルは、外部の依存関係を一切もちません。もし、次のシステムを読み込もうとした場合を考えてみましょう。

(asdf:load-system "project")

このシステムと、ファイル群(core.lisp, cl-ppcre, addons.lisp)が、適切な順番でよみこまれます。

別システムとの統合

では、あるパッケージをuseするとして、パッケージを提供(provide)するシステムが、同じ名前でない場合はどうでしょうか。例えば、closer-mopというシステムは、c2clというパッケージを提供(provide)します。ASDFがパッケージのためのシステムを見つけられるように、register-system-packagesという関数を呼び出しますが、その際はシステム名とそのパッケージ群を引数に含めます。project.asdの中に、次の一行を加えます。

(register-system-packages :closer-mop '(:c2cl))

最後のトリック

多くの場合、サブパッケージから全てのシンボルと一緒に1つのパッケージをエクスポート(export)したいでしょう。これは、ASDFに含まれるUIOPを使うことで解決します。例えば、all.lispという名前でファイルを定義してみましょう。

(uiop/package:define-package :project/all
  (:nickname :project)
  (:use-reexport :project/core :project/addons))

project/allシステムは、project/coreとproject/addonsの両システムに依存し、project パッケージに、そのシンボルを再度エクスポートします。

より詳しい情報

ASDF3やpackage-systemについて詳しく知りたい場合、次のリンクを参照してください。

Common Lispでsocketプログラミング(後編)

前回につづいて、Common Lispでsocketを扱うためのライブラリであるusocketの使い方について書かれた記事を紹介します。

前回の記事をまだお読みでない方は、本記事を読み進める前に、Common Lispでsocketプログラミング(前編)をお読みください。

今回の記事は、Smith Dhumbumroongさんの Socket programming in Common Lisp: how to use usocket’s with-* macrosという記事です。

今回は、前回のSidさんのコードを、with系マクロを使って書き直すと、どれだけコードが簡潔に、また安全になるかについて説明されています。では、読んでいきましょう。

Common Lispでsocketプログラミング:usocket with系マクロの使い方

最近、Common Lispでsocketプログラミングをしようと思い、(広く使われている)usocketライブラリを使うことにしました。

最初、socketライブラリのAPIの使い方に苦労しました。色々なチュートリアルや入門ガイドを読んで、with系マクロを使うことが推奨されていることを知りました。特に、Sid Heroorさんのこの投稿は参考になりました。特に、with-connected-socketマクロwith-client-socketマクロwith-server-socketマクロwith-socket-listenerマクロが推奨されていましたが、これらは、usocketライブラリを便利で明快に使えるように提供されているマクロです。これらのマクロは、エラーが適切に処理されたり、usocketを使い終わった後に、全てのsocketを適切に閉じることを保証してくれます。

問題は、これらのマクロを使うための入門ガイドやチュートリアルがなかったことでした。何時間もチュートリアルを探し回って無駄な時間を過ごしたあと、覚悟を決めて、これらのマクロのソースコードを読むことにしました。ソースコードを読んでみると、意外にもこれらのマクロは簡単に理解できるものでした。

基本的には、with-connected-socketマクロの上に、他のwith系マクロが作られています。with-connected-socketマクロの役割は、自動的にソケットを変数に束縛して、すべてのエラーと例外を処理するwith-mapped-conditionsマクロに本体(body)を渡して、処理から出るときに、そのsocketを破壊することを保証することです。with-client-socketマクロは、クライアントがサーバーにsocketで接続するのに便利なインターフェイスを提供してくれます。with-client-socketマクロは、自動でソケットを変数に束縛するだけでなく、クライアントにsocket streamも作ります。with-socket-listenerマクロは、socket-listen関数に対して、便利なインターフェイスを提供してくれて、結果として生じたsocketを、with-server-socketマクロに返します。with-server-socketマクロは、その返しとして、socket-listenが呼び出された結果作られたsocketを、with-connected-socketマクロに渡します。

前の記事でSidさんが書いた関数を、これらのwith系マクロで書き直すと、次のようになります。

サーバーのコード:

(defun start-simple-server (port)
  "Listening on a port for a message, and print the received message."
  (usocket:with-socket-listener (socket "127.0.0.1" port)
    (usocket:wait-for-input socket)
    (usocket:with-connected-socket (connection (usocket:socket-accept socket))
      (format t "~a~%" (read-line (usocket:socket-stream connection))))))

クライアントのコード:

(defun start-simple-client (port)
  "Connect to a server and send a message."
  (usocket:with-client-socket (socket stream "127.0.0.1" port)
    (format stream "Hello world!~%")
    (force-output stream)))

これらのマクロの組み合わせのおかげで、サーバーとクライアント両方のコード数を大幅に削減できることに注目してください。また、プログラムが、より安全に、また理解しやすくなります。これは、Common Lispのマクロの力を示す一例です。Pythonのような他の言語では。ここで見たようなwith系マクロに匹敵するものを導入しようとすれば、新しいバージョンが必要になることでしょう。一方、Lispでは、誰もが新しい制御構造を、ライブラリの一部として、導入できます。with系マクロの定義自体が、usocketで重要な関数を安全に使うためのガイドとして役割を果たしていることが気に入っています。

もう一度いいますが、Common Lispは、他のLisp方言と同様に、私に感銘を与えてくれます。これからもLispについてより多くのことを学ぶことが楽しみでなりません。

Common Lispでsocketプログラミング(前編)

Common Lispでsocketプログラミングをするには、usocketというライブラリが使われることが多いようです。

今回は、usocketの使い方を紹介している記事として、前編・後編に分けて、2つの記事をご紹介します。

まずは、Sid Heroorさんの記事 Short guide to TCP/IP Client/Server programming in Common Lisp using usocketsを紹介します。


この記事は、Common Lispで、TCP/IPクライアントサーバー型のプログラミングを書くための入門ガイドです。この入門ガイドでは、Common Lispライブラリusocketを用いて進めます。

Common Lispでsocketプログラミングをしてみようと思ったときに、先例がなかったので、この入門ガイドを書きました。丸一日を費やした後、自分でsocketプログラミングのコードを書いてみるという案を思いつきました。この記事は自分自身のために書いたものですが、あなたがsocketプログラミングを始めるのに役立つはずです。

では、quicklispでusocketライブラリを読み込んでください。

(ql:quickload "usocket")

まず、サーバーを作る必要があります。主に2つの関数を呼び出す必要があります。

usocket:socket-listen関数とusocket:socket-accept関数です。

usocket:socket-listen関数は、あるポートに結合(bind)して、そのポートの上で接続を待ちます。usocket:socket-listen関数は、socket objectを返します。接続が受け入れられるまで、usocket:socket-listen関数はsocket objectと一緒に待機します。

usocket:socket-accept関数は、socketオブジェクトを受け取ります。usocket:socket-accept関数はブロッキングコール(blocking call)で、接続が確立されたときにだけ値を返し、その接続に固有の新しいsocket objectを返します。私たちは、その接続を、クライアントと情報伝達をするために使います。

(defun create-server (port)
  (let* ((socket (usocket:socket-listen "127.0.0.1" port)) ; 引数のIPとportを結合してsocket objectを返す
     (connection (usocket:socket-accept socket :element-type 'character))) ; 接続が確立されたときにだけ値を返す 
    (unwind-protect 
     (progn
       (format (usocket:socket-stream connection) "Hello World~%")
       (force-output (usocket:socket-stream connection)))
      (progn
    (format t "Closing sockets~%")
    (usocket:socket-close connection)
    (usocket:socket-close socket)))))

ここで、私がおかした失敗についてお話しましょう。

1つ目の勘違いは、usocket:socket-accept関数は、stream objectを返すというものでした。正しくは、usocket:socket-accept関数は、socket objectを返します。振り返ると、その勘違いのせいで多くの時間をさいてしまいました。もしsocketを書くのであれば、新しいsocketからそれに対応するstreamをえる必要があります。socket objectは、stream slotを持っていて、ここではそれを明示的に利用します。どうすれば、それが分かるのでしょうか。次のようにすれば分かります。

(describe connection)

2つ目の間違いは、新しいsocketとサーバーのsocketを両方閉じる必要があるというものでした。これは明らかなことですが、最初に書いていたコードでは、その接続(connection)だけを閉じていたので、私はsocketを使用したままにし続けていました。もちろん、もう一つの選択肢としては、listenのときに、socketを拒絶することです。

これらの間違いをクリアできれば、あとは簡単です。コネクションとサーバーのsocketを閉じて終わりです。

次に、クライアントを作りましょう。このパートは簡単です。サーバーのポートに接続してください。そうすれば、サーバーからreadできるはずです。

ここで私がした間違えは、read-line関数ではなく、read関数を使ったことでした。そうしていたので、サーバーから"Hello"とだけ返っていました。すこし散歩に行ってから、間違えに気づき、コードを修正しました。

(defun create-client (port)
  (let ((socket (usocket:socket-connect "127.0.0.1" port :element-type 'character)))
    (unwind-protect 
     (progn
       (usocket:wait-for-input socket)
       (format t "~A~%" (read-line (usocket:socket-stream socket))))
      (usocket:socket-close socket))))

では、ここまでのコードをどう動かせばいいのでしょうか?2つのREPLを起動させてください。1つがサーバー用で、もう一つがクライアント用です。このファイルを両方のREPLで読み込んでください。

1つ目のREPLでサーバーを作りましょう。

 (create-server 12321)

では、2つ目のREPLでクライアントを走らせましょう。

 (create-client 12321)

2つ目のREPLで、"Hello World"と表示されれば、成功です。

こちらも参考にしてください。 1. UDP/IPの入門ガイド


この記事に、次のようなコメントがされていました。

「誰かにこの記事を読んでもらうつもりなら、usocket:with-client-socketマクロとusocket:with-server-socketマクロを使うことをお勧めします。usocket:with-client-socketマクロは、stream variableを結合する利点があるので、stream accessorsに気をつかう必要がありません。」

実際、usocketを使う場合は、with系マクロを使うことが多いようです。

次回は、Smith Dhumbumroongさんの記事Socket programming in Common Lisp: how to use usocket’s with-* macrosを紹介し、with系マクロを使うことで、どのようなコーディングエラーを防ぎ、またコードを簡素化できるのかを見ていきます。

Common Lispで小さなプロジェクトを始めるには?


本記事は、原著者の許諾のもと、翻訳・掲載しています。 Starting a minimal Common Lisp project / Phil Eaton


Common Lispで小さなプロジェクトを始めるには?

2018年3月5日, Phil Eaton

もし以前にLispについて少し話を聞いたり、学校でSchemeを習ったことがあったとしても、Common Lispは、あなたが想像するものとは全く違います。Schemeは、関数型プログラミングで人気ですが、Common Lispは、理論的に純粋なプログラミング言語というよりも、実世界での利用を強く意識して設計されたプログラミング言語です。さらに、人気がある処理系のSBCLは、高度に最適化されたコンパイラであり、Javaに対抗できるものです。

部品を組み立てる

Common Lispのシンボルは、第一級の変数(ラベル)であり、パッケージと呼ばれる名前空間に収められます。しかしパッケージは、ディレクトリを超える範囲は対処しません。ディレクトリを超えてソフトウェアを構成するには、ASDFの「システム」を使います。Common LispのパッケージはPythonのモジュール、ASDFのシステムはPythonのパッケージのようなものです。ASDFは、ローカル環境にない依存関係を管理しません。その用途にはQuicklispを使います。Quicklispは、事実上、Common Lispのパッケージ管理システムです。ASDFは、Common Lispの処理系に付属していることが多く、SBCLにも付属しています。Quicklispは、処理系に付属しません。

Quicklispを入手する

導入方法はQuicklispのサイトでも説明されていますが、基本的な手順は次の通りです:

$ curl -O https://beta.quicklisp.org/quicklisp.lisp
$ sbcl --load quicklisp.lisp
...
* (quicklisp-quickstart:install)
...
* ^D
$ sbcl --load "~/quicklisp/setup.lisp"
...
* (ql:add-to-init-file)

小さなパッケージ

これでプロジェクトを始める準備ができました。あなたが作りたいライブラリの名前で、ディレクトリ(フォルダ)を作ってください。例えばDockerのラッパーライブラリを作るとして、"cl-docker"というディレクトリを作りましょう。そのディレクトリの中には、".asd"の拡張子をつけて、同じ名前でファイルを作ります:

$ mkdir ~/projects
$ cd ~/projects
$ mkdir cl-docker
$ touch cl-docker/cl-docker.asd

ASDFは、ディレクトリの中で".asd"ファイルを探すので、".asd"ファイルは、ディレクトリと同じ名前をもつことが重要です。パッケージ化をする前に、まずはライブラリからエクスポートする関数を書きます。名前は重要ではありませんが、ここでは"cl-docker/docker.lisp"という名前でファイルを作り、以下のように編集してください:

(defun ps ()
  (let ((output (uiop:run-program '("docker" "ps") :output :string)))
    (loop for line in (rest (cl-ppcre:split "(\\n+)" output))
      collect (cl-ppcre:split "(\\s\\s+)" line))))

ここでは、uiopライブラリが使われています。uiopはASDFに組み込まれているので、明示的に読み込む必要はありません。uiopは、サブプロセスとして"docker ps"コマンドを実行して、出力を文字列として返します。その後、cl-ppcreライブラリから正規表現のsplit関数を使い、出力の先頭(output first)を行(lines)に分割します。最初の行以外の全てを取り込み、複数の行を、1個以上の空白文字(空白・タブ・改行文字)に基づいて分割します。

次に、Pythonでのモジュールに相当する「パッケージ」を定義しましょう。"cl-docker/package.lisp"のファイルを編集します。

(defpackage cl-docker
  (:use cl)
  (:import-from :cl-ppcre :split)
  (:export :ps))

ここでは、cl-dockerという名前でパッケージを定義しています。(:use cl)はCommon Lispのシンボルをパッケージにインポートすること、(:import-from :cl-ppcre :split) はcl-ppcreパッケージからsplitシンボルをインポートすること、(:export :ps)はps関数のみをエクスポートすることを意味します。

ここでは、"cl-docker/docker.lisp"の中に、そのファイルが、cl-dockerパッケージの一部であることを宣言する必要があります:

(in-package :cl-docker)

(defun ps ()
  (let ((output (uiop:run-program '("docker" "ps") :output :string)))
    (loop for line in (rest (cl-ppcre:split "(\\n+)" output))
      collect (cl-ppcre:split "(\\s\\s+)" line))))

次に、"cl-docker/cl-docker.asd"に、システムを定義しましょう。Common Lispのシステムは、Pythonのパッケージにあたります:

(defsystem :cl-docker
    :depends-on (:cl-ppcre)
    :serial t
    :components ((:file "package")
                 (:file "docker")))

これでASDFのシステムで必要なものを定義することができます: システムの名前、パッケージの定義、パッケージのコンポーネント("cl-docker/docker.lisp")を定義して、ASDFには"cl-ppcre"システムがディスク上で利用できるようにしておく必要があります。

また、ASDFにはコンポーネントが、書いた順番通りに実行されるようにする必要があります。(補足 :componentsの前に:serial tと書くことで、順番通りにコンポーネントが読み込まれます)

cl-ppcreシステム(依存関係があるシステム)がディスク上にない場合に備えて、Quicklispを通して間接的にシステムを読み込むことができます。Quicklispを使うと、依存関係にあって不足しているシステムをオンラインから取ってきます。

その前に、もしこのディレクトリが"~/common-lisp"にある場合を除いて、システムを読み込む際に、ASDFとQuicklispがどこを検索するかを知らせるために、自身のシステム定義があるディレクトリを登録する必要があります。このためには、"~/.config/common-lisp/source-registry.conf.d/"以下に、".conf"ファイルを次のように追加する必要があります。

(:tree "~/システムファイルがあるディレクトリ")

もし"cl-docker"というレポジトリが、"~/projects"ディレクトリに"cl-docker"ディレクトリとしてある場合、"~/.config/common-lisp/source-registry.conf.d/1-cl-docker.conf"を作って、次のように書きます:

(:tree "~/projects/cl-docker")

システムを使う

ここまで終えると、あなたのコンピュータのどこからでも、自作のライブラリを使うことができます。Common LispのREPLを開き、Quicklispでシステムを読み込みましょう。ローカルにない依存関係にあるシステムも読み込まれます:

$ sbcl
...
* (ql:quickload "cl-docker")
To load "cl-docker":
  Load 1 ASDF system:
    cl-docker
; Loading "cl-docker"
..................................................
[package cl-docker]
("cl-docker")
* (cl-docker:ps)

これで終わりです!

例のソースコードは、このGistを確認してください。

最後に

Common Lispは、使いやすくてパッケージも多くあり、成熟しています。ASDFでパッケージを設定するのは、Pythonで"setup.py"を設定するよりもシンプルです。

ASDFで依存関係にあるバージョンを固定する方法を説明しませんでしたが、それもできます。(参考: Qlotを参照ください。) ここで取り上げた方法はシンプルですが、Quicklispの作者のZach Beaneが開発しているquickprojectを使うと、雛形を作ってくれます。(参考: Fukamachiさんのcl-projectも同様の機能を提供しているライブラリです)

Common Lispの情報源

Practical Common Lispは、オンラインで無料で読むことができます。(参考: 邦訳は実践Common Lispとして出版されています) Practical Common Lispは、最高の情報源の1つであり、問題に直面したときに参考にし続けています。

Paul Graham氏のOn Lispも、Lispのマクロを理解しようとするときに必読の書籍です。 Schemeでマクロを書くときにも、役にたつでしょう。この本は絶版になっていますが、私は、LuluにPDFを送って、送料込みで20ドル以下でコピーを手に入れることができました。(参考: 邦訳はこのリンクから入手可能です。)

私は現在、Common Lisp the Language, 2nd Editionを読み進めています。こちらもオンラインで自由に閲覧できます。これはCommon Lispを実装しようと考えている人以外には、あまりおすすめできませんが、悪くないアイデアだと思います。

最後に、Peter Norvig氏のParadigms of Artificial Intelligence Programming (邦訳題: 実用Common Lisp - AIプログラミングのケーススタディ)がちょうどオンラインで無料で閲覧できるようになりました。私はまだ読んでいませんが、読むのを楽しみにしています。タイトルを見て恐れないでください。この書籍はCommon lispの実用的な案内書であり、伝統的なAIを取り上げています。

Schemeについて

私は、Chicken SchemeでWebプロトタイプをいくつか作ってみました、Chicken Schemeは、R5RS/R7RS準拠のScheme処理系です。Chicken Schemeは、Web開発で最善策とは思いません。ネイティブスレッドの対応もありませんし、(nginx等に)組み込みが容易にできる軽量のインタプリターがあるだけです。Chicken Schemeがニッチなのは、サードパーティーの良質なライブラリをもつ高品質の処理系であるからですが、Scheme処理系としては最速のSchemeとはいえません。

Scheme R6RSから派生したRacketを用いて、より大きなWebプロトタイプに取り組みました。Github issueを報告してくれるアプリを制作しました。Racketについては、ブログに気に入ったことを記事に書きました。

Racketは、JITコンパイラをもつ高性能なインタプリタで、スレッドのサポートもあり、サードパーティーのライブラリのコレクションも充実しています。Racketのエコシステムは、Haskelのと同じ問題があります。つまり、ライブラリと束縛(bindings)は、主に概念を証明するためだけであり、ドキュメントやテスト、実用性に欠けます。 FlaskでのJinjaのように、S式を用いてテンプレート化されたHTMLファイルをレンダリングしようとすると、悪夢を見ることになります。

Racketには申し訳ないですが...

最後に、Racketでのデバッグは楽しくありませんでした。バックトレースは、全く役に立ちませんでした。単純に考えると、この機能は、Racketが関数を最適化したり書き直したりするために使われると思います。 エラーを見つけたり修正したりすることができずに、Racketをうまく使うことができませんでした。

一方でCommon Lispは...

Common Lispは、処理系やエコシステムのおかげで堅固であり、開発も活発です。特にSBCLは、素晴らしいパフォーマンスとネイティブサポートがあり、バックエンドの開発において、有望な候補の一つです。

Caveman2でWebアプリを書いてみよう


本記事は、原著者の許諾のもと、翻訳・掲載しています。

Writing a Common Lisp Web App in caveman2 / Matthew Carter(ahungry)


次のリンクから、ページに飛びます。

Common LispでWebアプリを書いてみよう

Mito, Common Lisp ORM - README 訳


Fukamachiさんが開発されているMitoというORMのREADME訳です。 元の文章は、Mito READMEをご覧ください。


Mito

Mitoは、Integralの後継として開発中のORMです。

  • MySQL, PostgreSQL, SQLite3をサポート
  • RubyのActiveRecordのように、デフォルトでid(シリアル値),created_at,updated_atを追加
  • マイグレーション
  • データベース スキーマのバージョン管理

このソフトは開発初期段階です。APIは変更する可能性が高いので、注意してください。

SBCLとClozure CLの処理系での動作を想定しており、MySQL, PostgreSQL, SQLite3で動作します。

利用方法

(mito:connect-toplevel :mysql :database-name "myapp" :username "fukamachi" :password "c0mon-1isp")
;=> #<DBD.MYSQL:<DBD-MYSQL-CONNECTION> {100691BFF3}>

(defclass user ()
  ((name :col-type (:varchar 64)
         :initarg :name
         :accessor user-name)
   (email :col-type (or (:varchar 128) :null)
          :initarg :email
          :accessor user-email))
  (:metaclass mito:dao-table-class))
;=> #<MITO.DAO.TABLE:DAO-TABLE-CLASS COMMON-LISP-USER::USER>

(mito:table-definition 'user)
;=> (#<SXQL-STATEMENT: CREATE TABLE user (id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(64) NOT NULL, email VARCHAR(128))>)

(defclass tweet ()
  ((status :col-type :text
           :initarg :status
           :accessor tweet-status)
   (user :col-type user
         :initarg :user
         :accessor tweet-user))
  (:metaclass mito:dao-table-class))
;=> #<MITO.DAO.TABLE:DAO-TABLE-CLASS COMMON-LISP-USER::TWEET>

(mito:table-definition 'tweet)
;=> (#<SXQL-STATEMENT: CREATE TABLE tweet (id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, status TEXT NOT NULL, user_id BIGINT UNSIGNED NOT NULL, created_at TIMESTAMP, updated_at TIMESTAMP)>)

データベースへの接続

Mitoは、 RDBMSへの接続を確立するために、connect-topleveldisconnect-toplevelprovidesの関数を提供しています。connect-toplevelは、dbi:connectと同じ引数をとります。主に、ドライバーの種類、データベース名、ユーザ名とパスワードです。

(mito:connect-toplevel :mysql :database-name "myapp" :username "fukamachi" :password "c0mon-1isp")

connect-toplevel*connection*に新たな接続を設定し返します。

レキシカルに接続を使うには、次のようにバインドしてください:

(let ((mito:*connection* (dbi:connect :sqlite3 :database-name #P"/tmp/myapp.db")))
  (unwind-protect (progn ...)
    ;; Ensure that the connection is closed.
    (dbi:disconnect mito:*connection*)))

クラス定義

Mitoでは、(:metaclass mito:dao-table-class)を明記することにより、データベースのテーブルに対応するクラスを定義することができます。

(defclass user ()
  ((name :col-type (:varchar 64)
         :initarg :name
         :accessor user-name)
   (email :col-type (or (:varchar 128) :null)
          :initarg :email
          :accessor user-email))
  (:metaclass mito:dao-table-class))

上では、通常のCommon Lispでするようなクラス定義ですが、追加オプションも許可している点が異なります。

(defclass {class-name} ()
  ({column-definition}*)
  (:metaclass mito:dao-table-class)
  [[class-option]])

column-definition ::= (slot-name [[column-option]])
column-option ::= {:col-type col-type} |
                  {:primary-key boolean} |
                  {:inflate inflation-function} |
                  {:deflate deflation-function} |
                  {:references {class-name | (class-name slot-name)}} |
                  {:ghost boolean}
col-type ::= { keyword |
              (keyword . args) |
              (or keyword :null) |
              (or :null keyword) }
class-option ::= {:primary-key symbol*} |
                 {:unique-keys {symbol | (symbol*)}*} |
                 {:keys {symbol | (symbol*)}*} |
                 {:table-name table-name}
                 {:auto-pk boolean}
                 {:record-timestamps boolean}

クラスが自動的にスロットを追加することに注目してください。つまり、主キーがない場合にはidという名前の主キー、また、タイムスタンプのためにcreated_at``updated_atが追加されます。これらの振る舞いを無効化するには、defclassで、:auto-pk nil:record-timestamps nilと明記してください。

(mito.class:table-column-slots (find-class 'user))
;=> (#<MITO.DAO.COLUMN:DAO-TABLE-COLUMN-CLASS MITO.DAO.MIXIN::ID>
;    #<MITO.DAO.COLUMN:DAO-TABLE-COLUMN-CLASS COMMON-LISP-USER::NAME>
;    #<MITO.DAO.COLUMN:DAO-TABLE-COLUMN-CLASS COMMON-LISP-USER::EMAIL>
;    #<MITO.DAO.COLUMN:DAO-TABLE-COLUMN-CLASS MITO.DAO.MIXIN::CREATED-AT>
;    #<MITO.DAO.COLUMN:DAO-TABLE-COLUMN-CLASS MITO.DAO.MIXIN::UPDATED-AT>)

クラスは、暗黙的に、mito:dao-classを継承します。

(find-class 'user)
;=> #<MITO.DAO.TABLE:DAO-TABLE-CLASS COMMON-LISP-USER::USER>

(c2mop:class-direct-superclasses *)
;=> (#<STANDARD-CLASS MITO.DAO.TABLE:DAO-CLASS>)

これにより、全てのテーブルクラスに適用するメソッドを定義するときに便利になります。

テーブルの定義を生成する

(mito:table-definition 'user)
;=> (#<SXQL-STATEMENT: CREATE TABLE user (id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(64) NOT NULL, email VARCHAR(128), created_at TIMESTAMP, updated_at TIMESTAMP)>)

(sxql:yield *)
;=> "CREATE TABLE user (id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(64) NOT NULL, email VARCHAR(128), created_at TIMESTAMP, updated_at TIMESTAMP)"
;   NIL

データベースのテーブルを作る

(mapc #'mito:execute-sql (mito:table-definition 'user))

(mito:ensure-table-exists 'user)

CRUD

(defvar me
  (make-instance 'user :name "Eitaro Fukamachi" :email "e.arrows@gmail.com"))
;=> USER

(mito:insert-dao me)
;-> ;; INSERT INTO `user` (`name`, `email`, `created_at`, `updated_at`) VALUES (?, ?, ?, ?) ("Eitaro Fukamachi", "e.arrows@gmail.com", "2016-02-04T19:55:16.365543Z", "2016-02-04T19:55:16.365543Z") [0 rows] | MITO.DAO:INSERT-DAO
;=> #<USER {10053C4453}>

;; 上と同じです
(mito:create-dao 'user :name "Eitaro Fukamachi" :email "e.arrows@gmail.com")

;; 主キーの値を取得する
(mito:object-id me)
;=> 1

;; DBでデータを検索
(mito:find-dao 'user :id 1)
;-> ;; SELECT * FROM `user` WHERE (`id` = ?) LIMIT 1 (1) [1 row] | MITO.DB:RETRIEVE-BY-SQL
;=> #<USER {10077C6073}>

;; 更新する
(setf (slot-value me 'name) "nitro_idiot")
;=> "nitro_idiot"

(mito:save-dao me)
;-> ;; UPDATE `user` SET `id` = ?, `name` = ?, `email` = ?, `created_at` = ?, `updated_at` = ? WHERE (`id` = ?) (2, "nitro_idiot", "e.arrows@gmail.com", "2016-02-04T19:56:11.408927Z", "2016-02-04T19:56:19.006020Z", 2) [0 rows] | MITO.DAO:UPDATE-DAO

;; 削除する
(mito:delete-dao me)
;-> ;; DELETE FROM `user` WHERE (`id` = ?) (1) [0 rows] | MITO.DAO:DELETE-DAO
(mito:delete-by-values 'user :id 1)
;-> ;; DELETE FROM `user` WHERE (`id` = ?) (1) [0 rows] | MITO.DAO:DELETE-DAO

関係(Relationship)

関係(Relationship)を定義するには、スロットで:referencesを使います:

(defclass user ()
  ((name :col-type (:varchar 64)
         :initarg :name
         :accessor user-name)
   (email :col-type (or (:varchar 128) :null)
          :initarg :email
          :accessor user-email))
  (:metaclass mito:dao-table-class))

(defclass tweet ()
  ((status :col-type :text
           :initarg :status
           :accessor tweet-status)
   ;; This slot refers to USER class
   (user-id :references (user id)
            :initarg :user-id
            :accessor tweet-user-id))
  (:metaclass mito:dao-table-class))

;; USER-IDカラムの:col-typeは、外部のクラスから検索されます
(table-definition (find-class 'tweet))
;=> (#<SXQL-STATEMENT: CREATE TABLE tweet (
;        id BIGSERIAL NOT NULL PRIMARY KEY,
;        status TEXT NOT NULL,
;        user_id BIGINT NOT NULL,
;        created_at TIMESTAMP,
;        updated_at TIMESTAMP
;    )>)

関係(Relationship) を定義するために、:col-typeに別の外部クラスも特定できます。

(defclass tweet ()
  ((status :col-type :text
           :initarg :status
           :accessor tweet-status)
   ;; This slot refers to USER class
   (user :col-type user
         :initarg :user
         :accessor tweet-user))
  (:metaclass mito:dao-table-class))

(table-definition (find-class 'tweet))
;=> (#<SXQL-STATEMENT: CREATE TABLE tweet (
;        id BIGSERIAL NOT NULL PRIMARY KEY,
;        status TEXT NOT NULL,
;        user_id BIGINT NOT NULL,
;        created_at TIMESTAMP,
;        updated_at TIMESTAMP
;    )>)

;; :USER-IDの代わりに、:USER argを明記できます。
(defvar *user* (mito:create-dao 'user :name "Eitaro Fukamachi"))
(mito:create-dao 'tweet :user *user*)

(mito:find-dao 'tweet :user *user*)

例の後者では、USER-IDではなく、USERオブジェクトによって、TWEETを作成したり検索したりできます。

Mitoでは、テーブルを参照するのに外部キー制約を追加しません。その理由は、ORMを使うときに、問題になると確信できないからです。

InflationとDeflation

InflationとDeflationは、MitoとRDBMS間で値を変換する機能です。

(defclass user-report ()
  ((title :col-type (:varchar 100)
          :initarg :title
          :accessor report-title)
   (body :col-type :text
         :initarg :body
         :initform ""
         :accessor report-body)
   (reported-at :col-type :timestamp
                :initarg :reported-at
                :initform (local-time:now)
                :accessor report-reported-at
                :inflate #'local-time:universal-to-timestamp
                :deflate #'local-time:timestamp-to-universal))
  (:metaclass mito:dao-table-class))

Eager loading(事前にデータをロードする)

ORMを使うときの問題点として、N+1問題があります。

;; 悪い例

(use-package '(:mito :sxql))

(defvar *tweets-contain-japan*
  (select-dao 'tweet
    (where (:like :status "%Japan%"))))

;; Getting names of tweeted users.
(mapcar (lambda (tweet)
          (user-name (tweet-user tweet)))
        *tweets-contain-japan*)

この例では、"SELECT * FROM user WHERE id = ?" のように、クエリーを送ってユーザ情報を取得します。

このパフォーマンス上の問題が起きるのを防ぐために、includesをN個のクエリーではなく、単一のクエリを送る上のクエリにおきます。

;; GOOD EXAMPLE with eager loading

(use-package '(:mito :sxql))

(defvar *tweets-contain-japan*
  (select-dao 'tweet
    (includes 'user)
    (where (:like :status "%Japan%"))))
;-> ;; SELECT * FROM `tweet` WHERE (`status` LIKE ?) ("%Japan%") [3 row] | MITO.DB:RETRIEVE-BY-SQL
;-> ;; SELECT * FROM `user` WHERE (`id` IN (?, ?, ?)) (1, 3, 12) [3 row] | MITO.DB:RETRIEVE-BY-SQL
;=> (#<TWEET {1003513EC3}> #<TWEET {1007BABEF3}> #<TWEET {1007BB9D63}>)

;; No additional SQLs will be executed.
(tweet-user (first *))
;=> #<USER {100361E813}>

マイグレーション

(ensure-table-exists 'user)
;-> ;; CREATE TABLE IF NOT EXISTS "user" (
;       "id" BIGSERIAL NOT NULL PRIMARY KEY,
;       "name" VARCHAR(64) NOT NULL,
;       "email" VARCHAR(128),
;       "created_at" TIMESTAMP,
;       "updated_at" TIMESTAMP
;   ) () [0 rows] | MITO.DAO:ENSURE-TABLE-EXISTS

;; 変更がない場合
(mito:migration-expressions 'user)
;=> NIL

(defclass user ()
  ((name :col-type (:varchar 64)
         :initarg :name
         :accessor user-name)
   (email :col-type (:varchar 128)
          :initarg :email
          :accessor user-email))
  (:metaclass mito:dao-table-class)
  (:unique-keys email))

(mito:migration-expressions 'user)
;=> (#<SXQL-STATEMENT: ALTER TABLE user ALTER COLUMN email TYPE character varying(128), ALTER COLUMN email SET NOT NULL>
;    #<SXQL-STATEMENT: CREATE UNIQUE INDEX unique_user_email ON user (email)>)

(mito:migrate-table 'user)
;-> ;; ALTER TABLE "user" ALTER COLUMN "email" TYPE character varying(128), ALTER COLUMN "email" SET NOT NULL () [0 rows] | MITO.MIGRATION.TABLE:MIGRATE-TABLE
;   ;; CREATE UNIQUE INDEX "unique_user_email" ON "user" ("email") () [0 rows] | MITO.MIGRATION.TABLE:MIGRATE-TABLE
;-> (#<SXQL-STATEMENT: ALTER TABLE user ALTER COLUMN email TYPE character varying(128), ALTER COLUMN email SET NOT NULL>
;    #<SXQL-STATEMENT: CREATE UNIQUE INDEX unique_user_email ON user (email)>)

スキーマのバージョン管理

$ ros install mito
$ mito
利用法: mito コマンド [オプション...]

コマンド:
    generate-migrations
    migrate

オプション:
    -t, --type DRIVER-TYPE          DBI driver type (one of "mysql", "postgres" or "sqlite3")
    -d, --database DATABASE-NAME    Database name to use
    -u, --username USERNAME         Username for RDBMS
    -p, --password PASSWORD         Password for RDBMS
    -s, --system SYSTEM             ASDF system to load (several -s's allowed)
    -D, --directory DIRECTORY       Directory path to keep migration SQL files (default: "/Users/nitro_idiot/Programs/lib/mito/db/")
    --dry-run                       List SQL expressions to migrate

継承とミックスイン

DAO-CLASSのサブクラスは、継承されることができます。

このことは、似たようなカラムを持つクラスが必要なときに役に立ちます。

(defclass user ()
  ((name :col-type (:varchar 64)
         :initarg :name
         :accessor user-name)
   (email :col-type (:varchar 128)
          :initarg :email
          :accessor user-email))
  (:metaclass mito:dao-table-class)
  (:unique-keys email))

(defclass temporary-user (user)
  ((registered-at :col-type :timestamp
                  :initarg :registered-at
                  :accessor temporary-user-registered-at))
  (:metaclass mito:dao-table-class))

(mito:table-definition 'temporary-user)
;=> (#<SXQL-STATEMENT: CREATE TABLE temporary_user (
;        id BIGSERIAL NOT NULL PRIMARY KEY,
;        name VARCHAR(64) NOT NULL,
;        email VARCHAR(128) NOT NULL,
;        registered_at TIMESTAMP NOT NULL,
;        created_at TIMESTAMP,
;        updated_at TIMESTAMP,
;        UNIQUE (email)
;    )>)

どのデータベースのテーブルにも関連しないテーブルのためにテンプレートが必要な場合は、DAO-TABLE-MIXINを使えます。

(defclass has-email ()
  ((email :col-type (:varchar 128)
          :initarg :email
          :accessor object-email))
  (:metaclass mito:dao-table-mixin)
  (:unique-keys email))
;=> #<MITO.DAO.MIXIN:DAO-TABLE-MIXIN COMMON-LISP-USER::HAS-EMAIL>

(defclass user (has-email)
  ((name :col-type (:varchar 64)
         :initarg :name
         :accessor user-name))
  (:metaclass mito:dao-table-class))
;=> #<MITO.DAO.TABLE:DAO-TABLE-CLASS COMMON-LISP-USER::USER>

(mito:table-definition 'user)
;=> (#<SXQL-STATEMENT: CREATE TABLE user (
;       id BIGSERIAL NOT NULL PRIMARY KEY,
;       name VARCHAR(64) NOT NULL,
;       email VARCHAR(128) NOT NULL,
;       created_at TIMESTAMP,
;       updated_at TIMESTAMP,
;       UNIQUE (email)
;   )>)

トリガー

insert-dao, update-dao delete-daoは総称関数として定義されているので、:before :after :aroundメソッドを定義できます。

(defmethod mito:insert-dao :before ((object user))
  (format t "~&Adding ~S...~%" (user-name object)))

(mito:create-dao 'user :name "Eitaro Fukamachi" :email "e.arrows@gmail.com")
;-> Adding "Eitaro Fukamachi"...
;   ;; INSERT INTO "user" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) ("Eitaro Fukamachi", "e.arrows@gmail.com", "2016-02-16 21:13:47", "2016-02-16 21:13:47") [0 rows] | MITO.DAO:INSERT-DAO
;=> #<USER {100835FB33}>

インストール方法

$ mkdir -p ~/common-lisp
$ cd ~/common-lisp
$ git clone https://github.com/fukamachi/mito
$ ros -L ~/common-lisp/mito/mito.asd install mito
(ql:quickload :mito)

参考

作者

  • Eitaro Fukamachi (e.arrows@gmail.com)

Copyright

Copyright (c) 2015 Eitaro Fukamachi (e.arrows@gmail.com)

License

Licensed under the LLGPL License.

はじめてのClack - Common LispでWeb開発


本記事は、原著者の許諾のもと、翻訳・掲載しています。 Getting started with clack / Jason Miller

チュートリアルを始める前に、roswellをインストールしてください。

rowellをインストール後、TerminalでREPLを起動すると、準備完了です。

$ ros run
* 

では、チュートリアルに進みましょう。


はじめてのClack

Clackは、様々なLisp Webサーバを統一して利用するためのシンプルなフレームワークです。Clackに関する文献が少ないので、このページでは、Clackの使い方について書きます。

依存環境を読み込む

* (ql:quickload '(clack alexandria optima))
* (use-package :optima)

サーバを起動する

clackupに必要な引数はapplicationだけです。applicationは、(http-response-code http-headers-alist &optional `body)をリストで返す必要があります。

bodyには、(unsigned-byte 8)のベクタ、パス名、文字列のリストを書くことができます。

(defparameter *clack-server* (clack:clackup (lambda (env)
                                         '(200 nil ("Hello, World!")))))
Hunchentoot server is started.
Listening on localhost:5000.

curlでから始めましょう:

curl -s http://localhost:5000
Hello, World!

サーバを停止する

(clack:stop *clack-server*)

ハンドラを再定義する

サーバを毎回再起動するのは苦痛なので、再定義ができるハンドラを定義しましょう:

(defun handler (env) '(200 nil ("Hello World, redefinable!")))

そしてサーバを起動します。再定義が可能になるように、関数を名前で呼び出します:

(defparameter *clack-server*
  (clack:clackup (lambda (env) (funcall 'handler env))))
Hunchentoot server is started.
Listening on localhost:5000.

正常に動作するか確認しましょう:

curl -s http://localhost:5000
Hello World, redefinable!

では、再定義して、環境内でどうになっているかを見てみましょう:

(defun handler (env)
  `(200 nil (,(prin1-to-string env))))

結果を確認しましょう...

curl -s http://localhost:5000
(:REQUEST-METHOD :GET 
 :SCRIPT-NAME ""
 :PATH-INFO "/" 
 :SERVER-NAME "localhost"
 :SERVER-PORT 5000
 :SERVER-PROTOCOL :HTTP/1.1
 :REQUEST-URI "/"
 :URL-SCHEME "http"
 :REMOTE-ADDR "127.0.0.1"
 :REMOTE-PORT 53824
 :QUERY-STRING NIL
 :RAW-BODY #<FLEXI-STREAMS:FLEXI-IO-STREAM {1021B536E3}>
 :CONTENT-LENGTH NIL
 :CONTENT-TYPE NIL
 :CLACK.STREAMING T
 :CLACK.IO #<CLACK.HANDLER.HUNCHENTOOT::CLIENT {1021B537F3}>
 :HEADERS #<HASH-TABLE :TEST EQUAL :COUNT 3 {1021B53C13}>)

これがclackの中心部であり、環境を表すplistです。

環境についてのドキュメントは、lack documentにあります。

plistであることにより、捕捉される値は、destructuring-bind で処理できます。

(defun handler (env)
  (destructuring-bind (&key request-method path-info request-uri
                            query-string headers &allow-other-keys)
       env
     `(200
       nil
       (,(format nil "Method: ~S Path: ~S URI: ~A Query: ~S~%Headers: ~S"
                 request-method path-info request-uri query-string
                 (alexandria:hash-table-alist headers))))))
curl -s http://localhost:5000
Method: :GET Path: "/" URI: / Query: NIL
Headers: (("accept" . "*/*") ("user-agent" . "curl/7.53.0")
          ("host" . "localhost:5000"))

Optimaを使うと便利です:

(defun handler (env)
  (optima:match env
    ((guard (property :path-info path)
            (alexandria:starts-with-subseq "/foo/" path))
     `(200 nil (,(format nil "The path '~A' is in /foo/~%" path))))
    ((guard (property :path-info path)
            (alexandria:starts-with-subseq "/bar/" path))
     `(200 nil (,(format nil "The path '~A' is in /bar/~%" path))))
    ((property :path-info path)
     `(404 nil (,(format nil "Path ~A not found~%" path))))))
curl -s http://localhost:5000/foo/quux
curl -s http://localhost:5000/bar/quux
curl -s http://localhost:5000/baz/quux
The path '/foo/quux' is in /foo/
The path '/bar/quux' is in /bar/
Path /baz/quux not found

Public API

CLACK:CLACKUP

シンタックス:

    *clackup* app &key server port debug silent
    use-thread use-default-middlewares &allow-other-keys
    => ハンドラ

引数と値:

  • app - 1つの引数の関数を示します。lack.component:lack-componentのサブクラスです。パス名か文字列です。

  • server - シンボル。初期値は、:hunchentoot

  • port - 整数。初期値は、5000

  • debug - 論理値。初期値は、t

  • silent - 論理値。初期値は、nil

  • use-thread - 論理値。初期値は、スレッドサポートのシステムではt、そうでない場合はnil

  • use-default-middlewares - 論理値。初期値は、t

  • handler - clack.handler::handler

説明:

clackupは、指定されたサーバとportをバックエンドとして使いながら、サーバを起動します。

appは、次のように、サーバがハンドラの連鎖を構成するために使われます:

  • もしappが関数の場合は、appは直接つかわれます。そして、リクエスト環境を唯一の引数として、それぞれのリクエストに対して、呼び出されます。

  • applack.component:lack-componentのサブクラスの場合、(lack.component:call app environment)がリクエスト毎に呼び出されます。

  • appがパス名の場合、lispファイルとして扱われて実行されます。ファイルにある最後の式の結果が、上のように使われます。

  • appが文字列の場合、パス名に変換されて、上のように使われます。

  • use-default-middlewarestrueの場合、appはデフォルトのミドルウェアに内包(wrap)されます。

serverは、利用するバックエンドを指定します。バックエンドが見つからない場合、clackupは、quicklispasdfを用いて、バックエンドを読み込もうとします。

portは、サービスを受けつけるポートを特定します。

debugは、デバッグモードを指定します。ここでの結果は、バックエンドによって異なりますが、appの本体で生じる全てのエラーは、falseの場合、500のシスポンスを返すことで処理します。

silentは、ステータスのメッセージが出ないように抑えます。

use-threadtrueの場合, 別のスレッドでバックエンドが起動します。

(記事 終)


(参考) lack document より

環境 (Environment)

applicationは、環境(Environment)を受け取ります。中身はplistであり、以下のキーを含みます:

:request-method (必須, キーワード)
    HTTP リクエストメソッドとして、:GET :HEAD :OPTIONS :PUT :POST :DELETEを指定します。

:script-name (必須, 文字列)
    リクエストURIパスの最初の部分であり、Clack applicationに対応します。
    このキーの値は、クライアントがサーバーのルートでアクセスする場合、空の文字列になります。
    そうでない場合は、スラッシュ/から始めて指定します。

:path-info (必須, 文字列)
    リクエストURIパスの残りです。トレイリングスラッシュがない場合、空の文字列になります。

:query-string (オプション, 文字列)
   もし URIに?クエリがある場合、対応します。

:url-scheme (必須, 文字列)
    リクエストされるURIにより、"http"か"https"になります。

:server-name (必須, 文字列)
    名前解決されるサーバ名か、サーバのIPアドレスです。

:server-port (必須, 整数)
    リクエストが処理されているポート番号です。

:server-protocol (必須, キーワード)
    クライアントがリクエストを送るプロトコルのバージョンです。
    :HTTP/1.0か :HTTP/1.1 のことが多いです。

:request-uri (必須, 文字列)
    リクエストのURIです. 常に"/"で始まります。

:raw-body (オプション, ストリーム)
    リクエストの新しいbody部です。

:remote-addr (必須, 文字列)
    リモート用のアドレスです。

:remote-port (必須, 整数)
    リモート用のポートです。

:content-type (必須, 文字列)
    Content-Typeのヘッダー情報です。

:content-length (オプション, 整数)
    Content-Lengthのヘッダーの値です。

:headers (必須, ハッシュテーブル)
    ヘッダーのハッシュテーブルです。

CL Cookbook - loop


CL Cookbookの和訳です。loopの章の和訳を掲載します。


背景

loopマクロは、Common Lispで価値が高いのにも関わらず、ドキュメントが十分に整っていません。なぜ価値が高いのかというと、loopマクロは強力で、コンパクトで、map系の関数や再帰のようなものと比べて読みやすいコードになるからです。loopマクロでは、他の伝統的なプログラミング言語に慣れたプログラマーに親しみやすいスタイルを使います。

ここでは、loopマクロの使い方について説明します。loopマクロは、その内部にCやPascalのような複雑なシンタックスを持っている点で、大半のLispプログラムと異なります。loopマクロで書かれたコードを読むとき、脳の半分をLispモード、もう半分をPascalモードで考える必要があります。

loopマクロには4つのパーツがあると考えてください:

 1. 繰り返される変数を設定する式

 2. 条件により反復を行う式

 3. それぞれの反復に対して何かを行う式

 4. loopが終了する前に行う式

さらに、loopマクロは、値を返すこともできます。

loopマクロを使うときに、これらの全てのパーツを使うことはあまりありませんが、それぞれを様々な方法で組み合わせて使うことができます。

このチュートリアルの出典先は、Tutorial for the Common Lisp Loop Macroです。 Peter Karp氏の許可をえて掲載しています。

リストを通して反復し、それぞれの要素を出力します。

~~~lisp * (loop for x in '(a b c d e) do (print x) )

A B C D E NIL ~~~

2つのリストを交互に反復して、loopに返される値をコンスセルにまとめます。

~~~lisp * (loop for x in '(a b c d e) for y in '(1 2 3 4 5) collect (list x y) )

*1 ~~~

カウンタと値を用いて反復します。ここでの値は、反復のたびに計算されます。

~~~lisp * (loop for x from 1 to 5 for y = (* x 2) collect y)

(2 4 6 8 10) ~~~

リストを通して反復し、カウンターを交互に反復します。リストの長さは、反復が終了したときに決まります。2つのアクションが定義されており、そのうち1つは条件つきで実行されます。

~~~lisp * (loop for x in '(a b c d e) for y from 1

  when (> y 1)
  do (format t ", ")

  do (format t "~A" x)
  )

A, B, C, D, E NIL ~~~

if節を用いてloopを進めることもできます。

~~~lisp * (loop for x in '(a b c d e) for y from 1

  if (> y 1)
  do (format t ", ~A" x)
  else do (format t "~A" x)
  )

A, B, C, D, E NIL ~~~

testを用いて、loopを早く実行しましょう。アクションは、任意数の行で構成できます。また、loopのレキシカルスコープの外で定義された変数も参照できます。

~~~lisp * (loop for x in '(a b c d e 1 2 3 4) until (numberp x) collect (list x 'foo))

*2 ~~~

"While"も実行するかのチェックに使うことができます。"do"と"collect"はどちらも、1つの式にまとめることができます。

~~~lisp * (loop for x from 1 for y = (* x 10) while (< y 100)

  do (print (* x 5))

  collect y)

5 10 15 20 25 30 35 40 45 (10 20 30 40 50 60 70 80 90) ~~~

loopは、様々な方法でネスト(入れ子)にすることが可能です。

~~~lisp * (loop for x from 1 to 10 collect (loop for y from 1 to x collect y) )

*3 ~~~

複数の変数は、複合的なリストの要素を通して、ループすることができます。

~~~lisp * (loop for (a b) in '*4 collect (list b a) )

*5 ~~~

"return"アクションは、loopを止めて、結果を返すことができます。ここでは、文字列のsにある最初の数字を返します。

~~~lisp * (let *6 (loop for i from 0 below (length s) for ch = (char s i) when (find ch "0123456789" :test #'eql) return ch) )

\4

~~~

when/returnの組み合わせを短縮して書くことができるアクションもあります。

lisp * (loop for x in '(foo 2) thereis (numberp x)) T

lisp * (loop for x in '(foo 2) never (numberp x)) NIL

lisp * (loop for x in '(foo 2) always (numberp x)) NIL

*1:A 1) (B 2) (C 3) (D 4) (E 5

*2:A FOO) (B FOO) (C FOO) (D FOO) (E FOO

*3:1) (1 2) (1 2 3) (1 2 3 4) (1 2 3 4 5) (1 2 3 4 5 6) (1 2 3 4 5 6 7) (1 2 3 4 5 6 7 8) (1 2 3 4 5 6 7 8 9) (1 2 3 4 5 6 7 8 9 10

*4:x 1) (y 2) (z 3

*5:1 X) (2 Y) (3 Z

*6:s "alpha45"

Parenscript チュートリアル


本記事は、原著者の許諾のもと、翻訳・掲載しています。

Parenscript Tutorial / Vladimir Sedach


Parenscript チュートリアル

はじめに

このチュートリアルでは、LispからJavaScriptへのコンパイラであるParenscriptを用いて、簡単なWebアプリを開発します。Parenscriptの関数とマクロの詳細は、Parenscript Reference Manualを参照してください。

プロジェクトを始める

まず、Common Lispの処理系をインストールしましょう。SBCLは良い処理系です。

Common Lisp処理系の一覧は、CLikiCommon Lisp処理系一覧に記載されています。 次に、パッケージメネージャーのQuicklispをインストールしましょう。

このチュートリアルでは、以下のライブラリを使います:

CL-FAD   ファイル関連のユーティリティー CL-WHO   HTMLジェネレーター Hunchentoot   webサーバ Parenscript   JavaScriptジェネレーター

Quicklispで読み込みましょう:

(mapc #'ql:quickload '(:cl-fad :cl-who :hunchentoot :parenscript))

次に、チュートリアルのコードを収納するためのパッケージを定義しましょう:

(defpackage "PS-TUTORIAL"
  (:use "COMMON-LISP" "HUNCHENTOOT" "CL-WHO" "PARENSCRIPT" "CL-FAD"))
(in-package "PS-TUTORIAL")

CL-WHOでは、HTML属性をエスケープするかどうかを、自分で決めることができます。JavaScriptのインラインコード内でクオートされた文字列が、HTML属性の中で正しく動作するには、HTML属性にはダブルクォートを使い、JavaScriptの文字列にはシングルクォートを使うことで対処できます。

(setq cl-who:*attribute-quote-char* #\")

では、サーバを起動しましょう:

(start (make-instance 'easy-acceptor :port 8080))

psマクロは、S式でParenscriptのコードを受けとります。Parenscriptのコードは、Common Lispと同じように表現でき、マクロ展開時に、JavaScriptコードを含む文字列に可能な範囲で変換されます。

(define-easy-handler (example1 :uri "/example1") ()
  (with-html-output-to-string (s)
    (:html
     (:head (:title "Parenscript tutorial: 1st example"))
     (:body (:h2 "Parenscript tutorial: 1st example")
            "Please click the link below." :br
            (:a :href "#" :onclick (ps (alert "Hello World"))
                "Hello World")))))

WebページにParenscriptのコードを含めるには、HTMLタグにコードを埋め込みます:

(define-easy-handler (example2 :uri "/example2") ()
  (with-html-output-to-string (s)
    (:html
     (:head
      (:title "Parenscript tutorial: 2nd example")
      (:script :type "text/javascript"
               (str (ps
                      (defun greeting-callback ()
                        (alert "Hello World"))))))
     (:body
      (:h2 "Parenscript tutorial: 2nd example")
      (:a :href "#" :onclick (ps (greeting-callback))
          "Hello World")))))

別の方法としては、生成されたJavaScriptのコードを、別のHTTPリソースとしてサーブする(serve)ことでも可能です。このリソースへのリクエストは、ブラウザ内でキャッシュできます:

(define-easy-handler (example3 :uri "/example3.js") ()
  (setf (content-type*) "text/javascript")
  (ps
    (defun greeting-callback ()
      (alert "Hello World"))))

スライドショー

次に、より複雑な例に挑戦しましょう。スライドショーで画像を閲覧するアプリです。

まず、スライドショーを定義する方法が必要です。このチュートリアルでは、画像ファイルを含むフォルダを用意して、フォルダーごとのURLに基づいて、それぞれのスライドショーを提供します。/slideshows/{スライドの名前}でスライドショーを提供するために、独自のHunchentootハンドラを使います。また、/slideshow-images/{スライドショーの名前}/{画像のファイル名}にある画像をサーブするために、Hunchentootのfolder dispatcher関数を使います。

(defvar *slideshows* (make-hash-table :test 'equalp))

(defun add-slideshow (slideshow-name image-folder)
  (setf (gethash slideshow-name *slideshows*) image-folder)
  (push (create-folder-dispatcher-and-handler
         (format nil "/slideshow-images/~a/" slideshow-name)
         image-folder)
        *dispatch-table*))

マシン上で写真を探し出し、Hunchentootに写真をサーブさせましょう:

(add-slideshow "lolcat" "/home/junk/lolcats/")
(add-slideshow "lolrus" "/home/other-junk/lolruses/")

次に、スライドショーのWebページを作ります。JavaScriptを使うと、ページ全体を再読み込みせずに、スライドショーを見ることができます。また、JavaScpiptを有効にしていないブラウザでも通常のリンク移動を提供できます。どちらにせよ、スライドショーの視聴者に、スライドショーのシーケンス内での位置をブックマークしておきたいはずです。

サーバとブラウザ両方に、スライドショーの画像のための URIを生成する方法が必要です。defmacro+psマクロは、Common LispとParenscriptの間でのマクロ定義を共有します。defmacro+psマクロを使うと、コードの重複を省けます。

(defmacro+ps slideshow-image-uri (slideshow-name image-file)
  `(concatenate 'string "/slideshow-images/" ,slideshow-name "/" ,image-file))

次に、スライドショーのページをサーブするための関数を定義しましょう。ページは、/slideshow-images/{スライドショーの名前}のURIで画像をサーブされます。全てのページは、{スライドショーの名前}でディスパッチされる単独の関数によって処理されます。

JavaScriptを有効にしているブラウザは、インラインスクリプト内のps*関数によって生成されたスライドショーに関する情報をえることができます。ps*関数は、ランタイム時に生成されたコードを変換するために使われます。スライドショーの移動は、onclickハンドラで実行されます。onclickハンドラは、コンパイル時に、psマクロによって生成されます。

通常のHTMLのスライドショー移動は、クエリパラメータを用いて行われます。

(defun slideshow-handler ()
  (cl-ppcre:register-groups-bind (slideshow-name)
      ("/slideshows/(.*)" (script-name*))
    (let* ((images (mapcar
                    (lambda (i) (url-encode (file-namestring i)))
                    (list-directory
                     (or (gethash slideshow-name *slideshows*)
                         (progn (setf (return-code*) 404)
                                (return-from slideshow-handler))))))
           (current-image-index
             (or (position (url-encode (or (get-parameter "image") ""))
                           images
                           :test #'equalp)
                 0))
           (previous-image-index (max 0
                                      (1- current-image-index)))
           (next-image-index (min (1- (length images))
                                  (1+ current-image-index))))
      (with-html-output-to-string (s)
        (:html
         (:head
          (:title "Parenscript slideshow")
          (:script
           :type "text/javascript"
           (str
            (ps*
             `(progn
                (var *slideshow-name* ,slideshow-name)
                (var *images* (array ,@images))
                (var *current-image-index* ,current-image-index)))))
          (:script :type "text/javascript" :src "/slideshow.js"))
         (:body
          (:div :id "slideshow-container"
                :style "width:100%;text-align:center"
                (:img :id "slideshow-img-object"
                      :src (slideshow-image-uri
                            slideshow-name
                            (elt images current-image-index)))
                :br
                (:a :href (format nil "/slideshows/~a?image=~a"
                                  slideshow-name
                                  (elt images previous-image-index))
                    :onclick (ps (previous-image) (return false))
                    "Previous")
                " "
                (:a :href (format nil "/slideshows/~a?image=~a"
                                  slideshow-name
                                  (elt images next-image-index))
                    :onclick (ps (next-image) (return false))
                    "Next"))))))))

この関数は独自のハンドラなので、スライドショーのために新たにディスパッチャーを作る必要があります。関数オブジェクトの代わりに、ハンドラを命名するシンボルを渡す必要があります。そうすることで、ディスパッチャーに触れずに、ハンドラを再定義することができます。

(push (create-prefix-dispatcher "/slideshows/" 'slideshow-handler)
      *dispatch-table*)

最後に、/slideshow.jsを定義しましょう。

(define-easy-handler (js-slideshow :uri "/slideshow.js") ()
  (setf (content-type*) "text/javascript")
  (ps
    (define-symbol-macro fragment-identifier (@ window location hash))

    (defun show-image-number (image-index)
      (let ((image-name (aref *images* (setf *current-image-index* image-index))))
        (setf (chain document (get-element-by-id "slideshow-img-object") src)
              (slideshow-image-uri *slideshow-name* image-name)
              fragment-identifier
              image-name)))

    (defun previous-image ()
      (when (> *current-image-index* 0)
        (show-image-number (1- *current-image-index*))))

    (defun next-image ()
      (when (< *current-image-index* (1- (getprop *images* 'length)))
        (show-image-number (1+ *current-image-index*))))

    ;; use fragment identifiers to allow bookmarking
    (setf (getprop window 'onload)
          (lambda ()
            (when fragment-identifier
              (let ((image-name (chain fragment-identifier (slice 1))))
                (dotimes (i (length *images*))
                  (when (string= image-name (aref *images* i))
                    (show-image-number i)))))))))

@chainプロパティーは、 便利なマクロにアクセスします。(@ object slotA slotB)は、(getprop (getprop object 'slotA) 'slotB)に展開されます。chainも似ていますが、ネストされたメソッド呼び出しを提供します。

作者: Vladimir Sedach <vsedach@oneofus.la> 最終更新日: 2018-03-28


(追記)

  • 訳者は、macOSでチュートリアルを追試しました。ホームディレクトリに"junk/lolcats/"というディレクトリを作成して、jpeg形式で猫の画像を入れました。
  • スライドショーにアクセスするには、http://localhost:8080/slideshows/lolcatからアクセスしました。

CL Cookbook - パッケージ

---

原文は、[The Common Lisp Cookbook – Packages](http://lispcookbook.github.io/cl-cookbook/packages.html)です。g000001さんからご助言をいただき、`do-symbols`マクロについて加筆しました。

---

# パッケージにあるシンボルを列挙する

Common Lispには、パッケージ内のシンボルをまとめて把握するためのマクロがいくつかあります。特に興味深いのは、[`do-symbols`と`do-external-symbols`][do-sym]です。

`do-symbols`は、 該当するパッケージでアクセスできる全てのシンボルを列挙します。例えば、:clパッケージにある全てのシンボルを表示するには、次のように書きます。

~~~lisp
(do-symbols (s :cl) (print s))
~~~

`DO-EXTERNAL-SYMBOLS`は、exportされたシンボルだけを列挙します。"PACKAGE"という名前のパッケージでexportされた全てのシンボルを表示するには、次のように書きます。

~~~lisp
(do-external-symbols (s (find-package "PACKAGE"))
(print s))
~~~

また、リスト形式でシンボルを集めて表示するには、次のように書きます。

~~~lisp
(let (symbols)
(do-external-symbols (s (find-package "PACKAGE"))
(push s symbols))
symbols)
~~~

もしくは、[`LOOP`][loop]マクロを使って、次のように書くこともできます。

~~~lisp
(loop for s being the external-symbols of (find-package "PACKAGE")
collect s)
~~~

[guide]: http://www.flownet.com/gat/packages.pdf
[do-sym]: http://www.lispworks.com/documentation/HyperSpec/Body/m_do_sym.htm
[loop]: http://www.lispworks.com/documentation/HyperSpec/Body/06_a.htm

© 2018 t-cool