概要
ポエムです.
現状はこの記事の内容から変わっていたので,追加で記事を書きました.(2023年5月8日追記)
背景
Fortranでは,次(Fortran 2023)の次(Fortran 202Y)の規格にGenericsを入れる方向で検討が進んでおり,lfortranコンパイラにexperimental実装が追加されています.また,experimental実装を試してみたという記事も公開されています.
このtemplateの実装について,実際のFortranユーザの意見を聞くために,モダンFortran勉強会を開催しました.
現在Fortran202yでの導入に向けて仕様がされているgenericsについて、規格策定に携わっている日本の方から現状の仕様をご紹介いただきます。その後、参加者の皆様から、様々な目線で利用用途に合っているか等を議論する予定です。 検討段階の仕様は、lfortranにexperimentalとして実装されているので、動かしながら議論を深めていきます。
非常に熱い議論が行われ,予定時間を1時間以上超過するほどでした.
その議論の中で,個人的に感じたこと,個人的にこのように書きたいという要望を残すことにしました.
templeteの現状の例
lfortranに実装されているtemplate_add
を例に,現状の仕様を確認してみます.(仕様自体はまだ文書化されていません)
module template_add_m
implicit none
private
public :: add_t
requirement R(T, F)
type :: T; end type
function F(x, y) result(z)
type(T), intent(in) :: x, y
type(T) :: z
end function
end requirement
template add_t(T, F)
requires R(T, F)
private
public :: add_generic
contains
function add_generic(x, y) result(z)
type(T), intent(in) :: x, y
type(T) :: z
z = F(x, y)
end function
end template
contains
real function func_arg_real(x, y) result(z)
real, intent(in) :: x, y
z = x + y
end function
integer function func_arg_int(x, y) result(z)
integer, intent(in) :: x, y
z = x + y
end function
subroutine test_template()
instantiate add_t(real, func_arg_real), only: add_real => add_generic
real :: x, y
integer :: a, b
x = 5.1
y = 7.2
print*, "The result is ", add_real(x, y)
if (abs(add_real(x, y) - 12.3) > 1e-5) error stop
instantiate add_t(integer, func_arg_int), only: add_integer => add_generic
a = 5
b = 9
print*, "The result is ", add_integer(a, b)
if (add_integer(a, b) /= 14) error stop
end subroutine
end module
program template_add
use template_add_m
implicit none
call test_template()
end program template_add
まず,template内で参照される型と関数(いわゆるテンプレートパラメータ)を,requirement
として記述します.
requirement R(T, F)
type :: T; end type
function F(x, y) result(z)
type(T), intent(in) :: x, y
type(T) :: z
end function
end requirement
その後,テンプレート化したい関数を,template
構文の中に記述します.ここで,先ほどrequirement
ととして記述したR
をrequaire
によって取り込みます.add_generic
の引数と戻り値の型,add_generic
内部で呼んでいる関数F
のインタフェースが取り込まれます.
template add_t(T, F)
requires R(T, F)
private
public :: add_generic
contains
function add_generic(x, y) result(z)
type(T), intent(in) :: x, y
type(T) :: z
z = F(x, y)
end function
end template
テンプレート関数が定義されたら,テンプレートパラメータに具体的な型や関数を与えて実体化します.現状では,instantiate
文を用いることが想定されています.インスタンス化の処理について,私は
-
template add_t(T, F)
のコンストラクタadd_t
に型real
と関数func_arg_real
を与えてインスタンス化 - その中で実体化された
add_generic
に対してadd_real
という別名を与えている
と解釈しました.
instantiate add_t(real, func_arg_real), only: add_real => add_generic
real :: x, y
x = 5.1
y = 7.2
print*, "The result is ", add_real(x, y)
integer
型の和を計算するように実体化するには,下記のようにしています.
instantiate add_t(integer, func_arg_int), only: add_integer => add_generic
integer :: a, b
a = 5
b = 9
print*, "The result is ", add_integer(a, b)
func_arg_real
やfunc_arg_int
は,instantiate
する場所から見えるスコープで,下記のように定義されています.
real function func_arg_real(x, y) result(z)
real, intent(in) :: x, y
z = x + y
end function
integer function func_arg_int(x, y) result(z)
integer, intent(in) :: x, y
z = x + y
end function
例に対する私の感想
この例を見たとき,「C++のテンプレートと比較すると異様に複雑だ」という感想を持ちました.そもそも,func_arg_real
やfunc_arg_int
のように,“演算内容は同じだけと型(や配列次元)だけが異なる処理”を書きたくないからtemplateを使いたいのであって,この例のようにインタフェースを定義して,そのインタフェースに沿った実装を書かないといけないのであれば,総称名でいいんじゃないかと思います.
あくまでこれは例であって,requirement
に関数は必要なく,下記のように書くことが想定されていれば良いのですが…(Fortranらしく記述量が多いとは思いますが)
template add_t(T)
requires R(T)
private
public :: add_generic
contains
function add_generic(x, y) result(z)
type(T), intent(in) :: x, y
type(T) :: z
z = x + y
end function
end template
上記のように定義したテンプレートでは,lfortranで実行すると正しく加算が計算されません.少し心配です.
C++であれば,(テンプレートはいくらでも複雑にできることは知っているものの)この足し算を行う例は,もっと簡単に書けます.
template <typename T>
T add(T x, T y)
{
return x + y;
}
呼び出す場合にも,明示的なインスタンス宣言は必要ありません.今時は引数からT
を類推してくれるようで,<>
を使ったインスタンス化すら必要ではないようです.
int a=5, b=9;
float x = 5.1, y=7.2;
int c = add<int>(a, b);
float z = add<float>(x,y);
experimental実装に対応させれば,template <typename T>
に対応するのがrequirement
requirement R(T)
type :: T; end type
end requirement
テンプレート関数定義
T add(T x, T y)
{
return x + y;
}
に対応するのがtemplate
構文
template add_t(T)
requires R(T)
private
public :: add_generic
contains
function add_generic(x, y) result(z)
type(T), intent(in) :: x, y
type(T) :: z
z = x + y
end function
end template
なのだろうと判断できます.
もう少し簡単にならないものでしょうか?Fortran規格のGenericsを検討するチームは,(勉強会にも参加していただいた)HPCチームからの「もっと簡略化できないか?」との質問に対して,「C++のテンプレートの問題を踏まえてこのような記述にした」と回答したとのことです.
C++は,templateがらみで非常に大量のエラーを出すことが知られています.それを発生させないために,このような記述が要求されているのでしょうか?例えば,z = x + y
のように記述したとき,x, y
が+
演算が定義されていない型だった場合にエラーを出さないといけません.先ほどのadd<T>
のテンプレート型パラメータとしてstd::pair<int, int>
を与えてコンパイルしてみると,大量のメッセージが表示されます.
std::cout << add<std::pair<int, int>>(std::make_pair(1,1),std::make_pair(2,2)) << std::endl;
こういう問題を避けるために,x, y
に対する+
演算はユーザの責任で定義し,インスタンス化する際にテンプレートパラメータとして渡すことを強制しているのでしょか?そうだとすると,個人的には利便性を犠牲にしすぎているような気がします.
私は,parameterized derived typeの記述を拡張しつつ,CUDA Fortranのattribute
を取り込んで,
attribute(template(T)) add(x, y) result(z)
! ここでテンプレート型パラメータTを定義する.
! character(*), type :: Tとか,template, type :: Tとか
type(T), intent(in) :: x, y
type(T) :: z
z = x + y
end add
! インスタンス化
instantiate add_i => add(integer)
c = add_i(a, b)
! あるいは直接呼び出し
c = add(integer)(a, b)
! 内部手続や総称名のように,引数から自動で型パラメータを推定
c = add(a, b)
のように書けるとうれしいと思っています.あとは,スカラか配列かの区別もなくせるようになると,うれしいですね.
勉強会の中では,そもそもテンプレートライブラリを作ったとして,どのように計算機性能を引き出すのかが見えないというコメントもありました.テンプレートに具体的な型を当てはめて関数を生成するのは,コンパイラがコンパイル時に行います.そうすると,ユーザの指定したコンパイルオプションに合わせたコードを生成したり,ユーザが実行する計算機で高い実効性能を出そうとしたりすると,ソースを提供して,ユーザが作成したプログラムと共にコンパイルをしなければならないのではないか?という危惧です.他の言語ではどうしているのでしょうか?ご存じの方がいれば,ぜひ教えてください.
まとめ
私の要望は2点
- 異なる型,同じ演算をする関数を複数回書かせるのは止めてほしい
- 最悪
instantiate
が必要だとしても,テンプレートの定義はもう少し簡単にしてほしい