エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Elixir でビット列を展開する覚書

例えば右のように

11001010(2) = CA(16)

1111000011001100(2) = F0CC(16)

に Elixir で展開するための覚書です。

Elixir の内包表記は for

Elixir にも内包表記があります。他の関数型言語とくらべて内包表記っぽくない表記と感じましたが、意味するとろは確かに内包表記です。

iex> for n <- [1,2,3,4,5], do: n * n
[1, 4, 9, 16, 25]
iex> for x <- [1,3,5], y <- [2,4,6], do: {x, y}
[{1, 2}, {1, 4}, {1, 6}, {3, 2}, {3, 4}, {3, 6}, {5, 2}, {5, 4}, {5, 6}]

ちなみに for/1 はモジュール Kernel.SpecialForms で定義されています。

Elixir (と Erlang)の特徴はバイナリにも内包表記が使える点です。

iex> for <<c <- <<1, 2, 3, 4, 5>> >>, do: c * c                
[1, 4, 9, 16, 25]

個々の演算の値をバイナリにして、:into オプションを利用すれば、結果もバイナリで得ることができます。

iex> for <<c <- <<1, 2, 3, 4, 5>> >>, into: <<>>, do: <<c * c>>
<<1, 4, 9, 16, 25>>

また個々の演算の値がバイナリになっていればよいので、元のバイナリに対して長さの異なる結果を得ることができます。

iex> for <<c <- <<1, 2, 3, 4, 5>> >>, into: <<>>, do: <<c, c>> 
<<1, 1, 2, 2, 3, 3, 4, 4, 5, 5>>

加えて任意のサイズのビットで値を取り出すことができます。

iex> for <<bit::1 <- <<128>> >>, into: <<>>, do: <<bit>>
<<1, 0, 0, 0, 0, 0, 0, 0>>

バイナリはビット数を指定しない場合は 8 ビットで扱われるため、この <<bit>> は 8 ビットの値と解釈されます。 もちろんここでもビット数を指定することができるので、例えば次のようにすると元のバイナリが得られます。

iex> for <<bit::1 <- <<128>> >>, into: <<>>, do: <<bit::1>>
<<128>>

ビット列を展開する

必要な情報がそろったので、これらを踏まえて。

iex> x = 0xCA
202
iex> <<y::16>> = for <<bit::1 <- <<x::8>> >>, into: <<>>, do: << <<bit::1>>, <<bit::1>> >>
<<240, 204>>
iex> Integer.to_string(y, 16)                                                             
"F0CC"

CA(16) から F0CC(16) を得ることができました。

ビット列を文字列に展開する

Elixir の文字列はビット列です。

iex> "" == <<>>
true
iex> <<65>> == "A"
true

つまり文字列に対するいろいろな加工を内包表記を使って書くことができます。

iex> for <<c <- "hello">>, do: Integer.to_string(c, 16)
["68", "65", "6C", "6C", "6F"]
iex> for c <- [0x68, 0x65, 0x6C, 0x6C, 0x6F], into: "", do: <<c>>
"hello"

この仕組みを利用すればビット列を任意の文字列に展開することもできます。

iex> for <<bit::1 <- <<0x5A>> >>, into: "", do: if bit == 1, do: "@", else: "_"
"_@_@@_@_"

これを踏まえて。 フォントデータをキャラクターで表示してみます。

iex> [0x10, 0x28, 0x44, 0x82, 0xfe, 0x82, 0x82, 0x00] |> Enum.each(&IO.puts(for <<bit::1 <- <<&1>> >>, into: "", do: if bit == 1, do: "[]", else: "  "))
      []        
    []  []      
  []      []    
[]          []  
[][][][][][][]  
[]          []  
[]          []  

キャラクタ以外にも、たとえば 0<<0::24>>1<<0xffffff::24>> と展開すれば 2 値のデータから 24 ビットカラーのデータを作成することができます。

補足:Erlang の内包表記

Erlang にもリストとバイナリ両方の内包表記があります。こちらの方がよく見る内包表記の形式をしています。

> [X * 2 || X <- [1,2,3,4,5]].
[2,4,6,8,10]
> << <<(C* 2)>> || <<C>> <= <<1,2,3,4,5>> >>.
<<2,4,6,8,10>>
> [C * 2 || <<C>> <= <<1,2,3,4,5>>].         
[2,4,6,8,10]
> << <<(X * 2)>> || X <- [1,2,3,4,5]>>.
<<2,4,6,8,10>>

いつか読むはずっと読まない:失われたものを復元するという偉業

とうとう新種として判明し命名されました。

2019年9月現在、全身実物化石と全身復元骨格を間近で見ることができます。これはぜひ見て欲しい。

恐竜・古生物ビフォーアフター

恐竜・古生物ビフォーアフター

恐竜の魅せ方 展示の舞台裏を知ればもっと楽しい

恐竜の魅せ方 展示の舞台裏を知ればもっと楽しい

Timex.format のフォーマットの書式覚書

Elixir で日時を便利に操作する定番のパッケージ timex

そこで定義されている Timex.format/2 をいつも忘れてしまいます。

~N[2019-08-09 01:02:03.456789]
|> Timex.to_datetime("Asia/Tokyo")
|> Timex.format("{YYYY}/{0M}/{0D} {h24}:{m}:{s} {Zabbr}") 
# =>{:ok, "2019/08/09 01:02:03 JST"}

と、いうわけで。覚書として一覧にしてみました。

そして。一覧を作成中に Timex.format/3 で formatter に :strftime を指定すると strftime の書式で指定できるということを知りました。

~N[2019-08-09 01:02:03.456789]
|> Timex.to_datetime("Asia/Tokyo")
|> Timex.format("%Y/%m/%d %H:%M:%S %Z", :strftime) 
# => {:ok, "2019/08/09 01:02:03 Asia/Tokyo"}

一つ賢くなった。結果オーライ。

なお。timex には format/2, format/3 の他に、locale を指定できる lformat/3, lformat/4 という関数も用意されています。 こちらの出力も合わせて一覧にしました。

 ~N[2019-08-09 01:02:03.456789]
|> Timex.to_datetime("Asia/Tokyo")
|> Timex.lformat("{YYYY}/{0M}/{0D} {WDfull} {AM} {0h12}:{m}", "ja")     
# => {:ok, "2019/08/09 金曜日 午前 01:02"}
format string example of format example of lformat
YYYY 2019 2019
YY 19 19
C 20 20
WYYYY 2019 2019
WYY 19 19
M 8 8
0M 08 08
Mfull August 8月
Mshort Aug 8月
D 9 9
0D 09 09
Dord 221 221
Wiso 32 32
Wmon 31 31
Wsun 31 31
WDmon 5 5
WDsun 5 5
WDshort Fri
WDfull Friday 金曜日
h24 01 01
h12 1 1
0h12 01 01
m 02 02
s 03 03
s-epoch 1565280123 1565280123
ss .456789 .456789
am am 午前
AM AM 午前
Zname Asia/Tokyo Asia/Tokyo
Zabbr JST JST
Z +0900 +0900
Z: +09:00 +09:00
Z:: +09:00:00 +09:00:00
ISO:Extended 2019-08-09T01:02:03.456789+09:00 2019-08-09T01:02:03.456789+09:00
ISO:Extended:Z 2019-08-08T16:02:03.456789Z 2019-08-08T16:02:03.456789Z
ISO:Basic 20190809T010203.456789+0900 20190809T010203.456789+0900
ISO:Basic:Z 20190808T160203.456789Z 20190808T160203.456789Z
ISOdate 2019-08-09 2019-08-09
ISOtime 01:02:03.456789 01:02:03.456789
ISOweek 2019-W32 2019-W32
ISOweek-day 2019-W32-5 2019-W32-5
ISOord 2019-221 2019-221
RFC822 Fri, 09 Aug 19 01:02:03 +0900 金, 09 8月 19 01:02:03 +0900
RFC822z Thu, 08 Aug 19 16:02:03 Z 木, 08 8月 19 16:02:03 Z
RFC1123 Fri, 09 Aug 2019 01:02:03 +0900 金, 09 8月 2019 01:02:03 +0900
RFC1123z Thu, 08 Aug 2019 16:02:03 Z 木, 08 8月 2019 16:02:03 Z
RFC3339 2019-08-09T01:02:03.456789+09:00 2019-08-09T01:02:03.456789+09:00
RFC3339z 2019-08-08T16:02:03.456789Z 2019-08-08T16:02:03.456789Z
ANSIC Fri Aug 9 01:02:03 2019 金 8月 9 01:02:03 2019
UNIX Fri Aug 9 01:02:03 JST 2019 金 8月 9 01:02:03 JST 2019
ASN1:UTCtime 190808160203Z 190808160203Z
ASN1:GeneralizedTime 20190809010203 20190809010203
ASN1:GeneralizedTime:Z 20190808160203Z 20190808160203Z
ASN1:GeneralizedTime:TZ 20190809010203+0900 20190809010203+0900
kitchen 1:02AM 1:02午前

書式の解釈は、パッケージのコード上でそれぞれ Timex.Parse.DateTime.Tokenizers.Default , Timex.Parse.DateTime.Tokenizers.Strftime というモジュールの map_directive/2 という関数で実装されています。

Timex.Parse.DateTime.Tokenizers.Default.map_directive/2 Timex.Parse.DateTime.Tokenizers.Strftime.map_directive/2

ローマ数字を Elixir でパースする

Erlang/Elixir はバイナリのパースが得意ということで。

ローマ数字で書かれた数をパースする関数を書いてみました。

defmodule RomanNumerals do
  @moduledoc """
  Documentation for RomanNumerals.
  """

  def parse(roman) when is_binary(roman), do: parse(roman, 0)

  defp parse("", sum), do: sum
  defp parse(<<"M", rest::binary>>, sum), do: parse(rest, sum + 1000)
  defp parse(<<"CM", rest::binary>>, sum), do: parse(rest, sum + 900)
  defp parse(<<"D", rest::binary>>, sum), do: parse(rest, sum + 500)
  defp parse(<<"CD", rest::binary>>, sum), do: parse(rest, sum + 400)
  defp parse(<<"C", rest::binary>>, sum), do: parse(rest, sum + 100)
  defp parse(<<"XC", rest::binary>>, sum), do: parse(rest, sum + 90)
  defp parse(<<"L", rest::binary>>, sum), do: parse(rest, sum + 50)
  defp parse(<<"XL", rest::binary>>, sum), do: parse(rest, sum + 40)
  defp parse(<<"X", rest::binary>>, sum), do: parse(rest, sum + 10)
  defp parse(<<"IX", rest::binary>>, sum), do: parse(rest, sum + 9)
  defp parse(<<"V", rest::binary>>, sum), do: parse(rest, sum + 5)
  defp parse(<<"IV", rest::binary>>, sum), do: parse(rest, sum + 4)
  defp parse(<<"I", rest::binary>>, sum), do: parse(rest, sum + 1)
end
iex> RomanNumerals.parse("MCMXC")
1990

正しい形式の 3,999 までの値をパースできるはずです。

正しくない形式(LL とか XXXXXXX とか)もパースできてしまうので、それを受け付けないようにするにはもう一工夫必要。

いつか聴くはずっと聴かない:MCMXC a.D.

Enigma のファーストアルバム「MCMXC a.D.」。その名の通り、西暦 1990 年のリリース。

サッドネス(永遠の謎)

サッドネス(永遠の謎)

Elixirのプロセスの起動順について

Elixir で GenServer, Supervisor, mix deps.get で取得したパッケージの Application の起動順について調べたのでそのまとめ。

GenServer

GenServer.start_link/3 のドキュメントに次のように記述されています。

To ensure a synchronized start-up procedure, this function does not return until init/1 has returned.

Genserver.start_link/3

と、いうわけで。同じプロセスで複数の GenServer プロセスを起動するばあい、先に起動したプロセスの init/1 が完了してから後続のプロセスが起動することが保証できます。先に起動したプロセス宛に安全にメッセージを送信できると考えてよさそうです。

メッセージを受信する GenServer 。

defmodule Hoge.Foo do
  use GenServer

  require Logger

  def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
  def init(state), do: {:ok, state}

  def handle_cast({:foo, sender}, state) do
    Logger.info("#{__MODULE__} has received a message from #{sender}")
    {:noreply, state}
  end
end

メッセージを送信する GenServer 。

defmodule Hoge.Bar do
  use GenServer

  def start_link(opts) do
    receiver = get_in(opts, [:receiver])
    GenServer.start_link(__MODULE__, %{receiver: receiver})
  end

  def init(state) do
    GenServer.cast(state.receiver, {:foo, __MODULE__})

    {:ok, state}
  end
end

メッセージを受信する GenServer と送信する GenServer を順に起動します。

defmodule Hoge do
  def do_something do
    {:ok, foo_pid} = Hoge.Foo.start_link([])
    {:ok, bar_pid} = Hoge.Bar.start_link(receiver: foo_pid)
  end
end

実行。

$ iex -S mix
iex(1)> Hoge.do_something()         
18:21:48.782 [info]  Elixir.Hoge.Foo has received a message from Elixir.Hoge.Bar

この例ではほとんど処理時間がないためわかりにくいですが、Foo.init/1 で時間がかかる処理を記述してみても完了するまで Foo.start_link/1 は処理を待つので、Bar は安全にメッセージを Foo 宛に送信することができます。

Supervisor

Supervisor のドキュメントに次のように記述されています。

When the supervisor starts, it traverses all child specifications and then starts each child in the order they are defined.

start and shutdownSupervisor

また終了についても同じページの同じセクションに次のようにあります。

The shutdown process happens in reverse order.

監督する子プロセスの定義順に起動し、終了時はその逆順で停止されるようです。

Hoge を次のように書き換えます。

defmodule Hoge do
  def do_something do
    children = [
      Hoge.Foo,
      {Hoge.Bar, receiver: Hoge.Foo}
    ]

    opts = [strategy: :one_for_one, name: Hoge.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Hoge.Foo は名前付きで起動しているので GenServer.cast/2 は名前で呼び出すことができます。それを利用して、Hoge.Bar には送信先を指定するパラメータに名前を渡しています。

実行。

$ iex -S mix
iex(1)> Hoge.do_something
18:32:38.261 [info]  Elixir.Hoge.Foo has received a message from Elixir.Hoge.Bar

特に代わり映えはしないです、はい。

mix deps.get で取得した Application

例えば次のように FooBar がアプリケーションで、Hoge がそれらを取り込んでなおかつ起動順を指定したいばあいです。

./
├── bar/
│   └── lib/
│        ├── bar/
│        │   └── application.ex
│        └── bar.ex
├── foo/
│   └── lib/
│        ├── foo/
│        │   └── application.ex
│        └── foo.ex
└── hoge/
    ├── lib/
    │   ├── hoge/
    │   │   └── application.ex
    │   └── hoge.ex
    └── mix.exs

このようなばあいは、まず、 application/0 が返すパラメータに :included_application を追加してそこにアプリケーション名を記述します。

アプリケーション名は mix.exsproject/0:app で記載しているパラメータの値になります。モジュール名でないので注意してください。

defmodule Hoge.MixProject do
  use Mix.Project

  def project do
    [
      app: :hoge,
      version: "0.1.0",
      elixir: "~> 1.8",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  def application do
    [
      extra_applications: [:logger],
      included_applications: [:foo, :bar], # 取り込むアプリケーションの名前を記述
      mod: {Hoge.Application, []}
    ]
  end

  defp deps do
    [
      {:foo, path: "../foo"},
      {:bar, path: "../bar"}
    ]
  end
end

ドキュメントにある通り :included_applications に記載されたアプリケーションは自動的には起動しなくなります。

Any included application, defined in the :included_applications key of the .app file will also be loaded, but they won't be started.

*Application.start/2

取り込んだ側のアプリケーションで Supervisor などを利用して起動することで、アプリケーションの起動順を制御することができます。

たとえば。Foo.ApplicationBar.Application を次のように定義しておくと、

defmodule Foo.Application do
  use Application

  def start(_type, _args) do
    children = [
      Foo
    ]

    opts = [strategy: :one_for_one, name: Foo.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
defmodule Bar.Application do
  use Application

  def start(_type, args) do
    children = [
      {Bar, args}
    ]

    opts = [strategy: :one_for_one, name: Bar.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Hoge.Application に次のように記述することで、順序を指定して起動させることができるようになります。

defmodule Hoge.Application do
  use Application

  def start(type, _args) do
    children = [
      %{id: Foo.Application, start: {Foo.Application, :start, [type, nil]}},
      %{id: Bar.Application, start: {Bar.Application, :start, [type, [receiver: Foo]]}}
    ]

    opts = [strategy: :one_for_one, name: Hoge.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

子プロセスを定義する記述は、 Supervisor.start_link/2 のエラーメッセージに書かれた説明が一番わかりやすそうです。

If you own the given module, please define a child_spec/1 function
that receives an argument and returns a child specification as a map.
For example:

    def child_spec(opts) do
      %{
        id: __MODULE__,
        start: {__MODULE__, :start_link, [opts]},
        type: :worker,
        restart: :permanent,
        shutdown: 500
      }
    end

Note that "use Agent", "use GenServer" and so on automatically define
this function for you.

However, if you don't own the given module and it doesn't implement
child_spec/1, instead of passing the module name directly as a supervisor
child, you will have to pass a child specification as a map:

    %{
      id: Foo.Application,
      start: {Foo.Application, :start_link, [arg1, arg2]}
    }

メッセージにあるようにモジュールに child_spec/1 と定義するか、:id:start を持つ Map を渡すかするようにします。 上記の例では Map を渡しています。

いつか読むはずっと読まない: Futabasaurus suzukii

国内で最も有名な首長竜にも関わらず正式な学名がついたのは 2006 年。 「かはく」こと 国立科学博物館の日本館三階北翼 をぜひ訪ねてみてください。

フタバスズキリュウ もうひとつの物語

フタバスズキリュウ もうひとつの物語

Elm で mouse のイベントを取得する覚書

Elm のイベント用の関数は onClick など主だったものはライブラリで用意されていますが、それ以外のイベントは on 関数を利用して自分で合成する必要があります。

以下、合成方法の覚書です。

mousemove イベントをハンドリングする

イベントの値を受け取る型を用意します。

type Msg = Move Int Int

イベントのデコーダを用意します。

mousemove イベントのうち clientXclientY の二つのフィールドをそれぞれ Int として取得し、Move を適用するデコーダです。

map2 Move (field "clientX" int) (field "clientY" int)

map2, field, intJson.Decode で定義されている関数です。

動作を REPL で確認してみます。

$ elm repl
---- Elm 0.19.0 ----------------------------------------------------------------
Read <https://elm-lang.org/0.19.0/repl> to learn more: exit, help, imports, etc.
--------------------------------------------------------------------------------
> import Json.Decode exposing(map2, field, int, decodeString)
> type Msg = Move Int Int
> decodeString (map2 Move (field "clientX" int) (field "clientY" int)) "{\"clientX\":10,\"clientY\":20}"
Ok (Move 10 20)
    : Result Json.Decode.Error Msg
> 

{"clientX":10,"clientY":20} という JSON から Move 10 20 という値を取得することができました。

このデコーダon 関数に与えます。

イベントの構造については MDN などを参照してください。

実装

div 要素上のマウスカーソルの位置を表示するだけのサンプルです。

import Browser
import Html exposing (Html, div, span, text)
import Html.Events exposing (on)
import Html.Attributes exposing (style)
import Json.Decode exposing (map2, field, int)

main =
  Browser.sandbox
    { init = init
    , update = update
    , view = view
    }

type alias Model = { x: Int , y: Int }

init : Model
init = { x = 0 , y = 0 }

type Msg = Move Int Int

update : Msg -> Model -> Model
update msg model =
  case msg of
    Move x y -> {x = x, y = y}

view : Model -> Html Msg
view model =
  div []
    [ span
        []
        [ text ("(" ++ (String.fromInt model.x) ++ ", " ++ String.fromInt model.y ++ ")") ]
    , div
        [ style "background-color" "gray"
        , style "height" "80vh"
        , on "mousemove" (map2 Move (field "clientX" int) (field "clientY" int))
        ]
        []
    ]

合成する関数に名前をつける

見通しをよくするために名前をつけます。ここでは Move を引数で受け取るようにすることで他のメッセージにも利用できるようにしています。

-- view 以外は上と同じ

view : Model -> Html Msg
view model =
  div []
    [ span
        []
        [ text ("(" ++ (String.fromInt model.x) ++ ", " ++ String.fromInt model.y ++ ")") ]
    , div
        [ style "background-color" "gray"
        , style "height" "80vh"
        , onMouseMove Move
        ]
        []
    ]

onMouseMove : (Int -> Int -> msg) -> Html.Attribute msg
onMouseMove f =
  on "mousemove" (map2 f (field "clientX" int) (field "clientY" int))

外部で生成したデータをActive Storageでattachしたいとき

つまりこうゆうことです。

探した範囲でははっきりとした情報が見つからなかったので、めも。

このようなモデルがあるばあい、

class Article < ApplicationRecord
  has_one_attached :image
end

たとえばこのようにすることで storage にデータを格納できますが、Rails は元のデータとは別のオブジェクトを storage に生成します。

Article.find(id).image(filename: image_filename, io: File.read(image_filename))

worker が生成したデータの出力先を制御できるであれば、worker から storage へ直接出力させて Rails では attach 情報だけを更新することで実現できます。

blob =
  ActiveStorage::Blob.create(
    key: 'key-of-object',                # 格納したファイルのキー(実際に格納されているファイルのファイル名や S3 のキー)
    filename: 'something.jpg',           # ダウンロードする時のファイル名
    content_type: 'image/jpeg',          # content-type
    byte_size: 12345,                    # 格納したファイルのバイトサイズ
    checksum: 'TcZwnbeihC6rAB8p5LeRuQ==' # 格納したファイルのチェックサム
  )

Article.find(id).image.attach(blob)

チェックサムOpenSSL::Digest::MD5 で生成して Base64エンコードしたものです。

Base64.strict_encode64(OpenSSL::Digest::MD5.digest(格納したファイルの内容))

ローカルに格納されているばあい、キー(ファイル名)の先頭 4 文字を 2 文字ずつ使ったサブディレクトリに格納されます。

たとえば格納先のディレクトリが storage/ で格納するファイルのファイル名が ABCDEFGHIJKLMNOPQRSTUVWX だったばあい、次のように storage/AB/CD/ABCDEFGHIJKLMNOPQRSTUVWX というパスになります。

storage/
└ AB/
   └ CD/
      └ ABCDEFGHIJKLMNOPQRSTUVWX

CSSでモーダルを表示する覚え書き

CSS:chekced 擬似クラスを利用して表示を制御する方法です。

参考にさせていただいたサイトです。

上記のサイトでは要素を id で指定していますが、class で指定するように変更することで複数のモーダルの表示ができるようになります。

以下の例では同じページで二つのモーダルを表示させています。

コード

HTML

<label for="first-modal"><span>最初のモーダルを表示</span></label>
<label for="second-modal"><span>二つ目のモーダルを表示</span></label>

<div class="modal">
  <input id="first-modal" class="modal-checkbox" type="checkbox">
  <label for="first-modal" class="modal-close"></label>
  <div class="modal-content">
    <div>最初のモーダル</div>
  </div>
</div>

<div class="modal">
  <input id="second-modal" class="modal-checkbox" type="checkbox">
  <label for="second-modal" class="modal-close"></label>
  <div class="modal-content">
    <div>二つ目のモーダル</div>
  </div>
</div>

CSS (SASS)

// モーダルの土台
.modal {
  // ページの中央に配置
  margin: auto;
  width: 50%;
}

// モーダルの中身
.modal-content {
  // モーダルをページ外(ページの上部)に配置する
  position: fixed;
  top: 0;
  transform: translateY(-100%);

  // 表示する時のアニメーション
  transition: all 0.3s ease-in-out 0s;

  // 表示時の背景との表示順を指定
  z-index: 40;

  // 表示時の背景のグレイアウトに影響されないように背景色を設定
  background: #fff;

  // 見た目を綺麗に
  padding: 20px;
  border-radius: 8px;
  width: 640px;
}

// モーダルの背景(クリックするとモーダルを閉じる)
.modal-close {
  // 通常は非表示
  display: none;

  // ページ全体を黒色で覆う
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: #000;
  opacity: 0;

  // 表示時のフェードイン
  transition: all 0.3s ease-in-out 0s;

  // モーダルの背面に配置
  z-index: 39;
}

// 表示の制御をするためのチェックボックス
.modal-checkbox {
  // チェックボックス自体は非表示
  // (!important は必要な場合に設定してください)
  display: none !important;

  // チェックされたときの表示の設定
  &:checked ~ {
    .modal-content {
      // 表示位置をページ内に変更する
      transform: translateY(0);
      top: 100px;

      // 影をつける
      box-shadow: 6px 0 25px rgba(0, 0, 0, 0.16);
    }

    .modal-close {
      // 背景に薄く表示する
      display: block;
      opacity: 0.3;
    }
  }
}

明日の自分のための補足説明。

~ は兄弟要素を指しています。

実行

ページを開くと二つのキャプションが表示されます。

f:id:E_Mattsan:20190420132006p:plain

最初のキャプションをクリックした状態。

f:id:E_Mattsan:20190420131839p:plain

二つ目のキャプションをクリックした状態。

f:id:E_Mattsan:20190420131851p:plain

<label for="first-modal">&times;</label>