「Learning Functional Programming in Go」を読んだ

はじめに

Packt PublishingLearning Functional Programming in Go を読みました。

www.packtpub.com

この本は、Go で関数型プログラミングをやってみようというなかなかニッチな内容になっています。名前がアレなのでもしかしたら、Functional Programming in Scala みたいな内容を想像された方もいらっしゃると思いますが、確かに FP の重要性は伝えてくれるものの、言語の柔軟性を活かしたエレガントなコードを学ぶための本にはなっていないです。この本はどちらかというと「今の言語仕様つらい。じゃあどうするか」を学ぶための本です。

実はこの本を読み始めた当初は、err != nil をはじめとしたボイラープレートを排除して簡潔な記述を手に入れたいという願望がありました。それに対する解決策は一応書かれています。でも、どうも雰囲気が違うなというのは、Chapter1 を読んでいるときに気付きました。

この本が伝えようとしていること

では、この本で何が語られているかをざっくりと紹介しますね。

まず最初に、Go で FP をやるためには欠けているものがいくつかあります。一例を挙げると、

などです。

TCO がサポートされていないので、いくら再帰を末尾呼び出しで書いたとしてもコンパイラにガン無視され最適化してくれません。また、Generics が無いので関数を一般化することができずコピペの嵐となります。仮に interface{} で汎用的に処理しようとすれば今度はリフレクションやキャストなどの選択を迫られることになりパフォーマンス上問題になってしまいます。そう、どうあがいても命令型プログラミングとコピペという結末に収束してしまうのです。

だけど、Go には高階関数があるじゃないか!

ということで、以降は高階関数を使用した Functor、Monoid、Monad へと物語は進んで行きます。時折もし generics があったらなら、あんなことやこんなこともできるのにという著者の熱い想いに触れながら。

結局のところ、今の Go で FP をやるには色々妥協しなくちゃいけなくて、この本では「可能な限り関数型プログラミングスタイルを。必要に応じて命令型プログラミングスタイルを使用する」というスタンスで必死に抗い続けます。つらい。つらすぎる。

だが、そのつらさがオレに執念を与えた。だから、無かったことにするわけには行かなかったのだ。(岡部倫太郎)

その執念が、Go2 への活力となり、公式の Proposing Changes to Go で積極的に議論に参加するよう呼びかけています。

TCO だとこの辺。

github.com

generics だとこの辺ですか。

github.com

幸いにして、 Go 2 Draft Designs では、 generics の話が出てきているので、Go2 ではもう少し FP がやりやすくなるのかなと期待しています。

おわりに

この本はそれ以外にも SOLID原則やマイクロサービス、DDD、CQRS など、アーキテクチャレベルでのアプローチについても触れられていて、特に Go の Duck Typing 前提のデザインパターンは面白いなと思いました。また、Go2 に generics が入るのはメリットだけじゃなくてコンパイル時間が長くなるという懸念があることも触れています。それが今後どうなるのか注目したいところです。

個人的には、TCOgenerics もサポートされた Go2 で、改めて「Learning Functional Programming in Go 2」を出版していただきたいと思ってます。

追記(2018/08/31)

補足すると、本ではSOLID原則のことを紹介してますが、すべてが Go でできるわけじゃないので、例えばリスコフの置換原則について継承のない Go ではどう考えるかみたいな内容になっています。

HHKBの装備(記録用)

私は HHKB Professional2 Type-S を尊師スタイルで使用していますが、今回はその HHKB の装備について書きたいと思います。(至って普通装備ですが...)

f:id:shinharad:20180827234624j:plain

キーボードブリッジ

キーボードブリッジは、ノートPC本体のキーボードの上に HHKB を乗せて打鍵するためのキーボードカバーです。尊師スタイルには必須のアイテムです。

f:id:shinharad:20180827234739j:plain

f:id:shinharad:20180827234809j:plain

キーボードブリッジ|PFUダイレクト

HHKB吸振マットHG(Professionalシリーズ用)

吸振マットを HHKB の裏面に装着すると、打鍵の振動・反発を吸収してくれるので、タイピングが安定します。更に表面がエンボス加工になっていてキーボードブリッジに吸い付くように固定されるので、ノーマルのものより安定するんじゃないかなと思ってます(ノーマルは使ったことは無いですが...)あと、若干ですが打鍵音も静かになるようです。

f:id:shinharad:20180827234950j:plain

HHKB吸振マットHG(Professionalシリーズ用)|PFUダイレクト

ケーブル

昔の MacBook Pro(Early 2015) であれば、

  • USB miniBオス to タイプAオス(リール式)
  • 変換名人

を使用しています。ケーブルの方は100円均一で購入しました。

f:id:shinharad:20180827235426j:plain

一方、Type C な MacBook Pro 2017 以降の場合は、これを使っています。

ケーブルは、amazon星一つですが問題なく使えています。

終わりに

Type C to MiniB のケーブルは今のだとちょっとかっこ悪いのでまだまだ研究が必要そうです。それと今後はカラーキートップを付けてみたりと、至って普通のカスタマイズをしていこうかと思ってます。

Scalaの名前渡しパラメータが参照する度に評価されるってどこに明記されてるか調べてみた

恥ずかしい話なんですが、Scala の名前渡しパラメータ (by-name parameter) がメソッドの中でパラメータを参照する度に評価されてしまうことを知りませんでした。つまりこういうことですね。

def f: Int = {
  println("hello")
  0
}

def g(v: => Int) = {
  v + v
}

g(f)

// => 実行結果
// hello
// hello

幸いにして、今まで名前渡ししたパラメータをメソッドの中で複数回参照するような実装はしたことがなかったのですが、それにしても今まで何で気付けなかったのかなと思い、公式ドキュメントや書籍でどのように記載されているのかを調べてみました。

Scala Documentation

まずは、Scala Documentation ですが、名前渡しパラメータだとこのあたりですね。

このページでは次のようなサンプルコードが記載されています。

def whileLoop(condition: => Boolean)(body: => Unit): Unit =
  if (condition) {
    body
    whileLoop(condition)(body)
  }

var i = 2

whileLoop (i > 0) {
  println(i)
  i -= 1
}  // prints 2 1

確かにこれを見ると、名前渡しパラメータが毎回評価されることを前提とした実装になっています。また、

By-name parameters are only evaluated when used.

と書いてありますが、「使うときだけ評価される」というのが毎回という意味も含んでいたのか... まあちゃんと読み解けば気付けましたね...

Functional Programming Principles in Scala

次に CourseraFunctional Programming Principles in Scala。これの Lecture 1.3 - Evaluation Strategies and Termination の中で、Martin Odersky さんは以下のように仰ってます。

In Scala, we normally use call by value. You might ask, well, given the advantages of call by name that it terminates more often, why call by value? Well, it turns out that, for expressions in practice, call by value is often exponentially more efficient than call by name because it avoids this repeated recomputation of argument expressions that call by name entails. The other argument for call by value is that it plays much nicer with, imperative effects and side effects because you tend to know much better when expressions will be evaluated.Since Scala is also an, an imperative side, call by value is the standard choice. Except that, sometimes you really want to force call by name, and Scala lets you do that also. You do that by adding a, an arrow in front of the parameter type.

以下は、Coursera の和訳

Scalaでは通常は値渡しを使います。名前渡しに(引数に渡した無限ループが)停止しやすい利点があるのになぜ値渡しを使うのかと訝しむかもしれません。現実の式では値渡しの方が名前渡しよりも指数的に効率的であることが分かっています。なぜなら値渡しは名前渡しが必要とする引数の式の再計算が不要だからです。値渡しに有利になる別の主張はそれが命令的な作用や副作用をずっと上手く扱うことができるというものです。なぜならいつ式が評価されるのかがずっとわかりやすい傾向があるからです。Scalaもまた命令的な側面がありますので、値渡しが標準的な選択となります

めっちゃ言ってる...

値渡しは名前渡しが必要とする引数の式の再計算が不要だからです。

これも私が聞き逃してただけですねorz

Scalaスケーラブルプログラミング第3版

次にコップ本ですが、名前渡しパラメータについては「9.5 名前渡しパラメータ」に記載があって、ifwhile に近い制御構造を作る際にパラメータの predicate: () => Boolean から冗長な () を省略して predicate: => Boolean にしたのを名前渡しパラメータとして紹介されています。

確かに元の形が () => BooleanFunction0 と同じと考えれば毎回評価されるのは納得がいきます。 実際にこちらで紹介されているように名前渡しパラメータは Function0 に置き換わるようですしね。

まあそれは良いとして、本には「毎回評価されるよ」と明記されているわけじゃないので、気付けた人だけが知っているみたいなのはちょっと嫌だなと思ってました。

ところが、コップ本の最後の方を何となく見ていたらちゃんとそこに書いてあることに後から気付きました。コップ本の最後の方の「用語集」にはこんな感じで書かれています。

名前渡しパラメーター(by-name parameter)
たとえば(x: => Int)のように型の前に=>が付けられているパラメータ(仮引数)。名前渡しパラメーターに対応する実引数は、メソッドが呼び出される前ではなく、メソッド内でパラメータが名前で参照されるたびに評価される。名前渡しでないパラメータは、値渡しである。

これも私が見落としてただけでしたか...

おわりに

最近会社の若者たちに Scala を教えてるのですが、今回の件はその中で気付くことができました。なので、若者たちに感謝!

追記(2018/08/22)

Scala関数型デザイン&プログラミング ―Scalazコントリビューターによる関数型徹底ガイド」を確認したら「5.1 正格関数と非正格関数」で、名前渡ししたパラメータはキャッシュされないので毎回評価される。毎回評価させたくないのであれば明示的にlazyキーワードを使って値をキャッシュさせると書いてありました!もう一度全部読み返さないと!

ScalaでXMLを加工する

ScalaXML を扱う機会ってまだまだあるのかなと思ってます。

例えば、既存システムの WebAPI が XML 形式しか対応してなかった場合とか。そうしたときに、ScalaXML リテラルで直接 XML を書くとか、XML を探索して特定要素の値を抽出するとかだったら情報が比較的手に入れやすいと思います。コップ本にも書いてありますしね。

ところが、既にある XML に対して何らかの加工をしたい場合って情報が少ないんじゃないでしょうか。ということで、今回は ScalaXML を加工する方法について書きたいと思います。(ちなみに私は去年の開発でこの辺りけっこう悩んだ記憶があります。そのときは ここ に実装のヒントが書かれていることを知らなかったので...)

実行環境

実行した環境は以下になります。

準備

build.sbtlibraryDependenciesscala-xml を追加します。

libraryDependencies ++= Seq(
  "org.scala-lang.modules" %% "scala-xml" % "1.1.0"
)

実装概要

今回紹介するのは下記の実装についてです。

  • 要素に属性を追加する
  • 要素の内容を置換する
  • 子要素を置換する
  • 子要素を追加する
  • 指定した要素を削除する
  • 指定した属性の値が条件に合致する場合に要素を削除する
  • 子要素が空要素のみの親要素を削除する
  • XMLをトリムする

実装方法

要素に属性を追加する

特定の要素に属性を追加する方法です。名前空間なしの場合は UnprefixedAttribute名前空間ありの場合は PrefixedAttribute を使用します。

import scala.xml._

val xml =
  <root id="hoge">
    <a>aaa</a>
    <b>bbb</b>
  </root>

// 名前空間なし属性の追加
val result1 = xml % new UnprefixedAttribute("key", "value", xml.attributes)
println("名前空間なし:")
println(result1)
println

// 名前空間あり属性の追加
val result2 = xml % new PrefixedAttribute("x", "key", "value", xml.attributes)
println("名前空間あり:")
println(result2)

実行結果

名前空間なし:
<root id="hoge" key="value">
  <a>aaa</a>
  <b>bbb</b>
</root>

名前空間あり:
<root id="hoge" x:key="value">
  <a>aaa</a>
  <b>bbb</b>
</root>

要素の内容を置換する

要素の内容の置換には、scala.xml.transform.RuleTransformer を使用します。

RuleTransformer のコンストラクタに変換ルールを定義した scala.xml.RewriteRule を設定するのですが、具体的には transform メソッドの引数である Node のパターンマッチで、Elem クラスで label が指定した要素の名前だったら、copy メソッドで置換したい文字列を設定した新しい Elem を返すようにします。こうすることで、要素の内容が置換されます。

なお、scala.xml.RewriteRule は複数設定可能なので、変換ルールが分割されている場合にも対応できるようです。

import scala.xml._
import scala.xml.transform._

val xml =
  <root>
    <a>aaa</a>
    <b>bbb</b>
  </root>

val rule = new RuleTransformer(new RewriteRule {
  override def transform(n: Node): Seq[Node] = n match {
    case elem: Elem if elem.label == "b" =>
      elem.copy(
        child = elem.child collect {
          case Text(_) => Text("ccc")
        }
      )
    case _ =>
      n
  }
})

val result = rule.transform(xml)
println(result)

実行結果

<root>
  <a>aaa</a>
  <b>ccc</b>
</root>

子要素を置換する

要素の全ての子要素を置換したい場合は、copy メソッドで指定します。

val xml =
  <root>
    <a>aaa</a>
    <b>bbb</b>
  </root>

val result = xml.copy(child = <c>ccc</c>)
println(result)

実行結果

<root><c>ccc</c></root>

子要素を追加する

要素の指定した子要素を追加する場合は、既存の childList と同じ要領で新しい要素を追加します。

val xml =
  <root>
    <a>aaa</a>
    <b>bbb</b>
  </root>

val result = xml.copy(child = <c>ccc</c> ++ xml.child)
println(result)

実行結果

<root><c>ccc</c>
  <a>aaa</a>
  <b>bbb</b>
</root>

指定した要素を削除する

指定した要素を削除する場合はここでも scala.xml.transform.RuleTransformer を使用します。要素名が合致していたら NodeSeq.Empty に置き換えています。

import scala.xml._
import scala.xml.transform._

val xml =
  <root>
    <a>aaa</a>
    <b>bbb</b>
    <b>ccc</b>
  </root>

val rule = new RuleTransformer(new RewriteRule {
  override def transform(n: Node): NodeSeq = n match {
    case e: Elem if (e \\ "b") != NodeSeq.Empty => NodeSeq.Empty
    case _ => n
  }
})

val result = rule.transform(xml)
println(result)

実行結果

<root>
  <a>aaa</a>
  
  
</root>

NodeSeq.Empty に置き換えただけなので中身は歯抜けのようになります。この場合は後述しますがトリムをかけると良いです。

指定した属性の値が条件に合致する場合に要素を削除する

以下は、ある要素の配下で属性が flag="true" の要素を全て削除する例です。

import scala.xml._
import scala.xml.transform._

val xml =
  <root>
    <a>aaa</a>
    <b flag="true">bbb</b>
    <c flag="false">ccc</c>
    <d flag="true">ddd</d>
  </root>

val rule = new RuleTransformer(new RewriteRule {
  override def transform(n: Node): NodeSeq = n match {
    case e: Elem if (e \ "@flag").text == "true" => NodeSeq.Empty
    case _ => n
  }
})

val result = rule.transform(xml)
println(result)

実行結果

<root>
  <a>aaa</a>
  
  <c flag="false">ccc</c>
  
</root>

子要素が空要素のみの親要素を削除する

以下は、子要素が空要素、または存在しない場合は、親要素もろとも削除する例です。

import scala.xml._
import scala.xml.transform._

val xml =
  <root>
    <a>
      <aa />
    </a>
    <b>
      <ba>ba</ba>
      <bb></bb>
    </b>
    <c />
  </root>

val rule = new RuleTransformer(new RewriteRule {
  override def transform(n: Node): NodeSeq = n match {
    case elem: Elem if elem.child.count(_.toString.trim != "") == 0 => NodeSeq.Empty
    case _ => n
  }
})

val result = rule.transform(xml)
println(result)

実行結果

<root>
      
  <b>
    <ba>ba</ba>
    
  </b>
  
</root>

XMLをトリムする

先ほどまでの実行結果で、XML が歯抜けの場合はトリムをかけることできれいに整形されます。

val xml =
  <root>

    <a>aaa</a>

    <b>bbb</b>

  </root>

val trimXml = scala.xml.Utility.trim(xml)
println(trimXml)

実行結果

<root><a>aaa</a><b>bbb</b></root>

参考資料

gibo 2.x系で使い方が微妙に変わっていた

gibo は、github/gitignore で公開されている内容をもとに .gitignoreファイルを自動生成するツールですが、久しぶりに使ってみたら使い方が微妙に変わっていたので自分メモです。

インストール

mac の場合は homebrew でインストールします。

$ brew install gibo

その他の環境は 公式のREADME が参考になります。

バージョン確認

$ gibo version
gibo 2.1.0 by Simon Whitaker <sw@netcetera.org>
https://github.com/simonwhitaker/gibo

helpを確認

help を確認すると、.gitignore の生成方法が gibo dump に変わっているのが確認できます。以前は gibo Go >> .gitignore みたいな使い方でした。

$ gibo help
gibo 2.1.0 by Simon Whitaker <sw@netcetera.org>
https://github.com/simonwhitaker/gibo

Fetches gitignore boilerplates from https://github.com/github/gitignore

Usage:
    gibo [command]

Example:
    gibo dump Swift Xcode >> .gitignore

Commands:
    dump BOILERPLATE...   Write boilerplate(s) to STDOUT
    help                  Display this help text
    list                  List available boilerplates
    search STR            Search for boilerplates with STR in the name
    update                Update list of available boilerplates
    version               Display current script version

自動生成可能な言語や環境を列挙する

以前は gibo --list でしたが、gibo list に変わっています。

$ gibo list
=== Languages ===

Actionscript        Drupal          Jekyll          Perl            Stella
Ada         Eagle           Joomla          Perl6           SugarCRM
Agda            Elisp           Julia           Phalcon         Swift
Android         Elixir          KiCad           PlayFramework       Symfony
AppceleratorTitanium    Elm         Kohana          Plone           SymphonyCMS
AppEngine       EPiServer       Kotlin          Prestashop      Terraform
ArchLinuxPackages   Erlang          LabVIEW         Processing      TeX
Autotools       ExpressionEngine    Laravel         PureScript      Textpattern
C++         ExtJs           Leiningen       Python          TurboGears2
C           Fancy           LemonStand      Qooxdoo         Typo3
CakePHP         Finale          Lilypond        Qt          Umbraco
CFWheels        ForceDotCom     Lithium         R           Unity
ChefCookbook        Fortran         Lua         Rails           UnrealEngine
Clojure         FuelPHP         Magento         RhodesRhomobile     VisualStudio
CMake           Gcov            Maven           ROS         VVVV
CodeIgniter     GitBook         Mercury         Ruby            Waf
CommonLisp      Go          MetaProgrammingSystem   Rust            WordPress

~ 以下省略 ~

.gitignoreを自動生成

先ほど help でも確認しましたが、今は gibo dump を使うみたいですね。さっそく Go の .gitignoreファイルを自動生成してみましょう。

$ gibo dump Go >> .gitignore

$ cat .gitignore
/vendor/
/.idea
gibo 2.1.0 by Simon Whitaker <sw@netcetera.org>
https://github.com/simonwhitaker/gibo
### https://raw.github.com/github/gitignore/ea28c14da0faf75047165c10223635ba95566ad7/Go.gitignore

# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, build with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

生成できましたね。

今回は以上になります。

circeのdecodeでデフォルト値を設定する

ScalaJSONライブラリ circe で、JSON を decode する際にフィールドが欠落していたらデフォルト値に置き換える方法について書きます。結構前からある機能なのでご存知の方も多いかもしれません。

つまり、こういうケースですね。

import io.circe.generic.auto._
import io.circe.parser._

val json1 =
  """
    |{
    |  "name": "Taro",
    |  "body": "よろしくお願いします"
    |}
  """.stripMargin

val json2 =
  """
    |{
    |  "name": "Taro"
    |}
  """.stripMargin

final case class Message(
  name: String,
  body: String
)

// これはうまくいくけど
println(decode[Message](json1))
// => Right(Message(Taro,よろしくお願いします))

// フィールドが欠落しているので decode に失敗する。デフォルト値に置き換えたい。
println(decode[Message](json2))
// => Left(DecodingFailure(Attempt to decode value on failed cursor, List(DownField(body))))

実行環境

今回実行した環境は以下になります。

  • Scala 2.12.6
  • circe 0.9.3

実現方法

方法1

最初は、case class のそれぞれのフィールドにデフォルト値を設定しておき、io.circe.generic.extras.Configuration の指定で欠落するフィールドをデフォルト値へ置き換える方法です。

build.sbt は以下のように、基本セットの他に circe-generic-extras を依存に追加します。

libraryDependencies ++= Seq(
  "io.circe" %% "circe-core",
  "io.circe" %% "circe-generic",
  "io.circe" %% "circe-parser",
  "io.circe" %% "circe-generic-extras"
).map(_ % circeVersion)

val circeVersion = "0.9.3"

そして、ポイントとなるのは、

  • io.circe.generic.auto._ ではなく、io.circe.generic.extras.auto._ を import する
  • implicit val customConfig: Configuration = Configuration.default.withDefaults を定義する

コードを見てみましょう。

import io.circe.generic.extras.auto._
import io.circe.generic.extras.Configuration
import io.circe.parser._

val json1 =
  """
    |{
    |  "name": "Taro"
    |}
  """.stripMargin

val json2 =
  """
    |{
    |  "body": "よろしくお願いします"
    |}
  """.stripMargin

val json3 = "{}"

// case class のフィールドにはデフォルト値が設定されている
final case class Message(
  name: String = "匿名希望",
  body: String = "ノーコメントです"
)

implicit val customConfig: Configuration = Configuration.default.withDefaults

println(decode[Message](json1)) // => Right(Message(Taro,ノーコメントです))
println(decode[Message](json2)) // => Right(Message(匿名希望,よろしくお願いします))
println(decode[Message](json3)) // => Right(Message(匿名希望,ノーコメントです))

このように欠落するフィールドに対してデフォルト値が置き換わりました。

方法2

もう一つの方法は、デフォルト値を設定しておいた case class を別途用意しておき、それで置き換えるやり方です。

build.sbt は 方法1 とは異なり、circe-generic-extras を依存に追加する必要はありません。

libraryDependencies ++= Seq(
  "io.circe" %% "circe-core",
  "io.circe" %% "circe-generic",
  "io.circe" %% "circe-parser"
).map(_ % circeVersion)

val circeVersion = "0.9.3"

そして、こうに書きます。

import io.circe.generic.auto._
import io.circe.parser._

val json1 =
  """
    |{
    |  "name": "Taro"
    |}
  """.stripMargin

val json2 =
  """
    |{
    |  "body": "よろしくお願いします"
    |}
  """.stripMargin

val json3 = "{}"

// 方法1と違ってここではデフォルト値は設定していない
final case class Message(
  name: String,
  body: String
)

// 適用させるデフォルト値
lazy val defaultValue = Message("匿名希望", "ノーコメントです")

println(decode[Message => Message](json1).map(_(defaultValue)))
// => Right(Message(Taro,ノーコメントです))

println(decode[Message => Message](json2).map(_(defaultValue)))
// => Right(Message(匿名希望,よろしくお願いします))

println(decode[Message => Message](json3).map(_(defaultValue)))
// => Right(Message(匿名希望,ノーコメントです))

この方法でも欠落するフィールドに対してデフォルト値を置き換えることができました。ちなみに以下の issue にもあるように、方法1 の方が公式のやり方っぽいですね。

参考

HHKBでIntelliJ IDEAのショートカットキーのポジションを見直してみる

今年4月に MacBook Pro(以下「MBP」)の標準キーボードから Happy Hacking Keyboard Professional2 Type-S(以下「HHKB」)へ乗り換えました。独特なキー配列に最初は戸惑いを感じましたがそれは数日で慣れ、今では静電容量無接点方式の心地良い打鍵感で快適なタイピングライフを送っています。

ところで、私は普段の開発に IntelliJ IDEA を使用しているのですが、HHKB に変えたことで一部のショートカットキーの指のポジションを見直す必要がありましたので、今回はそんな話をしたいと思います。誰得な話ですが...

なお、対象は以下とします。

それと、今回取り上げるショートカットキーは以下です。

  • Rename (Shift + F6)
  • Find Usages (Option + F7)
  • Terminal (Option + F12)
  • File Structure (Command + F12)

Rename (Shift + F6)

変数名やメソッド名などを変更するショートカットキーですが、MBPでホームポジションをなるべく崩さずに打つと次のようになると思います。(写真の青枠は左手、赤枠は右手を意味しています。旧式MBPなのはお気になさらず...)

  • 左手小指で左 Shift
  • 右手人差し指で F6

f:id:shinharad:20180806010742p:plain

ところが、HHKB だと F6 は、Fn と 6 を同時に打たなければならなく、しかもこの二つのキーは物理的に離れているため右手だけでは打つことができません。つまり、こうするのはかなり無理があります。(画像の出典元: Happy Hacking Keyboard キー配列

f:id:shinharad:20180806010903p:plain

そこで、このように変えました。

  • 左手人差し指で 6
  • 右手薬指で右 Shift
  • 右手小指で Fn

f:id:shinharad:20180806010936p:plain

Find Usages (Option + F7)

次に、変数やメソッドの参照先を検索する Find Usages ですが、MBPで打つ場合、

  • 左手親指で左 Option
  • 右手人差し指で F7

f:id:shinharad:20180806011518p:plain

これも Rename と同じく右手だけで F7 は無理なので、こうしました。

  • 左人差し指で 7
  • 右手親指で右 Option
  • 右手小指で Fn

f:id:shinharad:20180806011530p:plain

Terminal (Option + F12)

IntelliJ IDEA のターミナルを開閉するショートカットキーですが、MBPで打つ場合、

  • 左手親指で左 Option
  • 右手小指で F12

f:id:shinharad:20180806011636p:plain

これに関しては、右手小指で Fn を押すことを考えると、これが自然な気がしてます。

  • 右手小指で Fn
  • 右手中指で F12
  • 右手親指で Option

f:id:shinharad:20180806011646p:plain

File Structure (Command + F12)

最後にファイル構造をポップアップで表示してくれる File Structure ですが、MBPで打つ場合、

  • 左手親指で左 Command
  • 右手小指で F12

f:id:shinharad:20180806011733p:plain

これは、Terminal と同じ要領で、こうにしました。

  • 右手小指で Fn
  • 右手中指で F12
  • 右手親指で Command

f:id:shinharad:20180806011809p:plain

おわりに

最後まで読んでいただいてありがとうございます。いかがだったでしょうか。
ホームポジション崩れまくりで敗北感すら感じてしまうわけですが、今のところこれが一番しっくりくるので、しばらくはこれでやっていこうと思います!