Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

簡潔な文章を書くコツ

リモートワークの普及でテキストベースのコミュニケーションが増えてきました。簡潔で伝わりやすい文章を書いて同僚と差をつけましょう!

なんで簡潔な文章を書くのか?

人間は文章を"読めない"から。 読まれる前提で書いてはいけない。テキストコミュニーケーションをする時は特に気をつけてください。

簡潔な文章の書き方

  • 結論から書く
  • 文章を短くする
  • 単語をできるだけ削る
  • 指示語をなくす
  • 理由を書くな
  • 意味を一意に定める
  • そもそも文法をちゃんとする

結論から書く

まず結論を書きましょう。伝えたいことをズドンと書く。御託はそのあと並べてください。

例えば次の2文はどちらが読みやすいですか?

  • 「あとどのくらいかかりそう?」「こういう仕様にかなり実装コストがかかりそうなので遅れそうです」
  • 「あとどのくらいかかりそう?」「遅れそうです。こういう仕様にかなり実装コストがかかりそうです。」

どちらが良いか、一目瞭然ですよね。後者です。まず質問に答えましょう。御託を並べるな。

文章を短くする

1文あたり20文字くらいを目安に文章を分割しましょう。30文字以上ある文章は大抵どこかで区切れます(区切れないケースもあります)。

コツとしては、接続詞で区切るとやりやすいです。

  • 「〜ですが、」→「〜です。しかし、」
  • 「〜であるため、」→「〜です。そのため、」

短文を心がけると機械翻訳で正確に訳されやすいメリットもあります。とにかく短くしましょう。

単語をできるだけ削る

単語を削れば文章が短くなります。短い文章は読まれる確率が上がります。 意味も分かり易いです。

削れる単語の一例を挙げます。

  • かなり
  • という
  • こと
  • (動詞)をすることができます → (動詞)られます

特に「かなり」や「すごく」は気を抜くと使いがちです。しつこく消していきましょう。

文章を短くするには、同じ意味でもっと短く書けないか常に考える癖をつけると良いです。

指示語をなくす

指示語(こそあど)があると、読み手に「ここにある『これ』は何を指しているんだ?」と思わせます。この記事を読んでる時点であなたは文章の書き方が分かっていません。指示語を使えるなんて驕らないでください。

書き手が指示語を置き換えるのは簡単な作業です。「これ」「それ」は文章から絶対に消しましょう。

理由を書くな

ほとんどのケースで相手は理由を知りたくありません。切りましょう。「理由」は相手が訪ねてきた時だけ書きましょう。

一意に定まるかを意識する

前後の文脈から単語の意味が明らかでないなら、一意に定まるよう修正しましょう。

一意に定まりづらい例を挙げます

  • 一般的な単語
  • 略語
  • 目的語を略す

逆に、一意に定まるものを挙げます

  • これまで何度も登場している

一意に定まるようにするには、修飾語をつける、省略をやめると良いです。

そもそも文法はちゃんとしよう

主語と述語がおかしくなっていないか?助詞の使い方は問題ないか?主語を省略した場合、相手は主語を理解できるか?このあたりはしっかり抑えましょう。まず文法がしっちゃかめっちゃかだと、その時点で読みづらいです。

Extra: 相手が知らない前提で書く

文章は基本的に相手が知らない前提で書きましょう。

例: サーバー起動しておいてください

サーバーの起動方法を相手が知っていれば問題ありません。しかし、知らない可能性がある場合は手順を示すとなお良いです。

改善案: サーバー起動しておいてください。手順はこちらのwikiページに書いてあります(リンク)

もちろん相手が絶対に知っているとわかっていれば不要です。しかし、基本的には知らない前提が良いでしょう(特にチームを跨ぐ場合)。

遅延読み込みリストをカンタンに作りたい

React で遅延読み込み機構を作ってみました。

デモ

github.com

コード

useShownuseUpdateHeight という2つのフックを使って実現します。useShown はスクロール状況から「読み込みを開始すべきか」を判定します。useUpdateHeight はコンポーネントの高さをストアへ伝えます。

import {
  FC,
  useEffect,
  useRef,
  useState,
  createContext,
  Children,
  useCallback,
} from 'react';

const HeightContext = createContext<
  [number[], (index: number, height: number) => void]
>([[], () => null]);
const IndexContext = createContext(0);

/**
 * height store をつなぐ処理と、index を各要素に与えるためのラッパー
 */
const LazyLoadWrapper: FC = ({ children }) => {
  const [height, setHeight] = useState<number[]>([]);
  const updateHeight = useCallback((index: number, height: number) => {
    setHeight((arr) => {
      const newValue = arr.concat();
      newValue[index] = height;
      return newValue;
    });
  }, []);
  return (
    <HeightContext.Provider value={[height, updateHeight]}>
      {Children.map(children, (child, index) => (
        <IndexContext.Provider value={index} key={index}>
          {child}
        </IndexContext.Provider>
      ))}
    </HeightContext.Provider>
  );
};

/**
 * 遅延読み込みされるコンポーネント
 */
const Child = () => {
  const divRef = useRef<HTMLDivElement>(null);
  const shown = useShown();
  const updateHeight = useUpdateHeight();
  const height = useRef((Math.floor(Math.random() * 9) + 1) * 100).current;

  useEffect(() => {
    if (divRef.current && shown) {
      updateHeight();
    }
  }, [shown, updateHeight]);

  if (!shown) {
    return null;
  }
  return <div style={{ width: 100, height }} ref={divRef}></div>;
};

export const App= () => {
  return (
    <LazyLoadWrapper>
      <Child />
      <Child />
      <Child />
      <Child />
      <Child />
    </LazyLoadWrapper>
  );
};

各コンポーネントで高さを伝達し、LazyLoadWrapper で囲むだけで遅延読み込みが実現できます。

useShown と useUpdateHeight の実装

結構シンプルです。useShown は「まだマウントしてない要素のうち一番上にあって、かつスクロール量が閾値を超えているか」を判定しているだけです。閾値は適当に「画面の高さ * 3」だけの余裕が取れるようにしています。

useUpdateHeight はさらにシンプルで、HeightContext の配列を更新するだけです。

import { useEffect, useState, useCallback, useContext } from 'react';

/**
 * 「表示するべきか否か」を返すフック。一度 shown が true になったら false になることはない。
 * height store の更新はしない。
 */
const useLazyload = () => {
  const index = useContext(IndexContext);
  const [height] = useContext(HeightContext);
  const [shown, setShown] = useState(false);

  useEffect(() => {
    if (shown) {
      return;
    }
    const nextIndexToMount = (() => {
      const indexOfFirstUndefined = height.findIndex(
        (item) => typeof item === 'undefined'
      );
      if (indexOfFirstUndefined !== -1) {
        return indexOfFirstUndefined;
      }
      return height.length;
    })();

    // 先頭から連続したマウント済み要素の高さの総和
    const totalHeight = height
      .slice(0, nextIndexToMount)
      .reduce((a, b) => a + b, 0);
    const listener = () => {
      if (
        nextIndexToMount === index &&
        totalHeight < 3 * window.innerHeight + window.scrollY
      ) {
        setShown(true);
      }
    };
    window.addEventListener('scroll', listener);
    return () => {
      window.removeEventListener('scroll', listener);
    };
  }, [height, index, shown]);
  return shown;
};

/**
 * コンポーネントの高さをストアへ伝えるためのフック。
 */
const useUpdateHeight = () => {
  const index = useContext(IndexContext);
  const [, setHeight] = useContext(HeightContext);
  return useCallback(
    (height: number) => setHeight(index, height),
    [index, setHeight]
  );
};

Jest でステートを持ったモジュールをテスト毎に初期化したい

private でステートを持ったモジュールのテストをするとき、初期化用のメソッドを作っていませんか? 実は jest では内部ステートをリセットする方法があります。

やり方

やり方は単純で、require('./myModule') でモジュールを読み込み、各テストの前に jest.resetModules() を行うだけです。

例えばメモ化した関数のテストを考えます。当然、各テスト毎にメモ化状況はリセットしたいです。

export const memoizedFunc = memoize(f);
// import { memoizedFunc } from './myModule'; // これの代わりに
describe('memoizedFunc', () => {
  // beforeEach でテスト毎に require し直す
  let memoizedFunc;
  beforeEach(() => {
    jest.resetModules();
    memoizedFunc = require('./myModule').memoizedFunc;
  });
  test('case1', () => {
    /* ステートがリセットされている */
  });
  test('case2', () => {
    /* ステートがリセットされている */
  });
});

let memoizedFunc; から始まる5行を足すだけでテスト自体は書き直す必要がありません。簡単ですね。

参考

javascript - how to reset module imported between tests - Stack Overflow

zx を使っていい感じに SIGINT を捌く

シェルスクリプトで ctrl + C を押して中断したときのクリーンアップ処理を書くのはそこそこ大変です(頑張ればできますが)。今回は zx を使った、クリーンアップ処理を含むコードの書き方を紹介します。

そもそも zx とは?

alt シェルスクリプト的なものです。JS ファイル内にシェルスクリプトを書くことができます。

import { $ } from 'zx';
const has = (command) => $`type "${command}" > /dev/null 2>&1`.then(() => true, () => false)

if (await has('rbenv')) {
  await $`rbenv init -`;
}

…まあ上のコードが読みやすいかはともかく、 async/await が使えたり、JS の各種メソッドが使えるので非常に便利です。

zx を使って SIGINT を捌いてみる

この processWithCleanup という関数を使ってコードを書いていきます。

import { $ } from 'zx';
import process from 'process';
const processWithCleanup = async (f, cleanup) => {
  process.on('SIGINT', () => {});
  try {
    await f();
  } catch (e) {
    await cleanup();
    throw e;
  }
};

この関数は、受け取った関数 f を実行し、SIGINT を受信したら cleanup を実行します(SIGINT かどうか判別してないですが)。cleanup 後に throw e しているので、このあとどう処理するか任せることができます。

実例

const main = async () => {
  await processWithCleanup(
    async () => {
      await $`git stash`;
      await $`docker build -t hoge ./`;
    },
    () => $`git stash pop`
  );
  $`git stash pop`;
  await $`docker push hoge`;

  await $`deploy`;
};
main().catch(console.error);

この例ではビルドして Docker Hub へプッシュし、最後にデプロイコマンドを実行します。ビルド中にエラーが起きたらデプロイは走りません。

pubsub パターンに対する思いと現時点でのベタープラクティス

なんらかの payload を渡したいとき、インターフェイスを定められない。これに尽きる。静的型付けとも相性が悪い。

payload と言ってるのは要するに window.addEventListener(eventName, event => {}) の event のことだ。TypeScript はかなり頑張って型付けしてくれているが、もし pubsub.addEventListener(event, () => {}) を自作したら型を付けるのがしんどいことは想像に難くない。window.addEventListener もカスタムイベントの event に型をつけるのは面倒だ。

多分ベタープラクティス

ベストではない気がするのでベター。

// 外部にエクスポートしない。
class PubSub {
  private listeners: { [key in PropertyKey]?: ((payload?: any) => void)[] } =
    {};
  addEventListener(eventName: string, callback: (payload?: any) => void) {
    if (!this.listeners[eventName]) {
      this.listeners[eventName] = [];
    }
    this.listeners[eventName]?.push(callback);
  }
  removeEventListener(eventName: string, callback: (payload?: any) => void) {
    this.listeners[eventName] = this.listeners[eventName]?.filter(
      (item) => item !== callback
    );
  }
  dispatchEvent(eventName: string, payload?: any) {
    for (const listener of this.listeners[eventName] || []) {
      listener(payload);
    }
  }
}

const clickEvent = 'BUTTON_CLICK';
const buttonPubsub = new PubSub();
export const buttonObserver = {
  click: () => buttonPubsub.dispatchEvent(clickEvent),
  addClickListener: (callback: () => void) => {
    buttonPubsub.addEventListener(clickEvent, callback);
    return () => buttonPubsub.removeEventListener(clickEvent, callback);
  },
};

このように、buttonOberver を介して PubSub を操作することで、addEventListener と dispatchEvent に対して適切な型をつけられる。さらに、PubSub インスタンスを都度生成すれば、他の Observer とカスタムイベント名が衝突しない。

ただし、PubSub クラスを外部へ露出させない都合上、モジュールへの切り出しが行えないデメリットがある。eslint-plugin-import などでアクセス制限を敷いても良いが、規模感次第だろう。また、テストをする際もステートを都度リセットしなければならず、若干コード量が増える。