【Go言語入門】6.関数についてさくっと学ぶ

これまで様々な開発を行ってきて基本的なことを振り返ることもなかったので、丁度いい機会ということで、初心に返ってGoにおいての関数を一つずつ復習しました。

前回の記事

定義

funcキーワードとシグネチャ用いて定義できます

シグネチャ(signature)とは関数名パラメータ名パラメータの型戻り値の型などのことをいいます。

実行する際は関数名(引数)のような形で使用します。

これまでに使ってきたfmt.Println(“hello”)も立派な関数です。

package main

import "fmt"

var flag bool

func print(v string) {
  fmt.Println(v)
}

func double(v float64) float64 {
  return v * 2
}

func invert() {
  flag = !flag
}

func main() {
  print("Hello world")
  fmt.Printf("double(2) * double(2) = %v\n", double(2)*double(2))
  fmt.Printf("flag is %v\n", flag)
  invert()
  fmt.Printf("flag is %v\n", flag)
}

// >>> Hello world
// >>> double(2) * double(2) = 16
// >>> flag is false
// >>> flag is true

一通り挙げてみました。少し雑ですが、関数に引数を与えてあげると各々が処理し、結果を返します。

別記事でも触れましたが、何気なく使ってきたmainも関数の一つで、処理を走らせると最初に実行されるように決められている特別な関数です。

書き忘れておりましたが、

func add(x int, y int) int {
  return x + y
}
↓
func add(x, y int) int {
  return x + y
}

変数の定義同様、関数の引数も型が同じであればまとめることができます。

defer

deferステートメントは渡した関数(fn)の実行を、呼び出し元の関数(Fn)が結果を返すまで遅延させることができます。

この時、渡した引数については即時評価されています。

package main

import "fmt"

func main() {
  defer fmt.Println("fuga")
  fmt.Println("hoge")
}

// >>> hoge
// >>> fuga

このような形で実行順序が変わっているのがわかると思います。

では複数遅らせた場合はどうでしょうか。

package main

import "fmt"

func main() {
  defer fmt.Println(2)
  defer fmt.Println(3)
  defer fmt.Println(4)
  fmt.Println(1)
}

// >>> 1
// >>> 4
// >>> 3
// >>> 2

deferに渡していない関数は当然最初なので1として、他の関数を上から2,3,4と出力させてみました。

すると、結果は逆順になりました。

これはdeferへ渡した関数が複数ある場合、スタックされた処理がLIFOの順番に実行されていくからです。

LIFOとは、Last In, First Outの略で、日本語では後入れ先出しと言われますが、これがdeferにおいて暗黙的に行われています。

何か直感的ではない気もしますが、もっと深入りしていけば理由がわかっていくのかも知れません。

Function values

まず大前提として忘れてはいけないのが、関数も変数であるということです。

つまり今まで使ってきた変数みたく、代入が可能で引数や演算、返り値にも出来るということです。

package main

import (
  "fmt"
  "math"
)

var hypotenuse = func(x, y float64) float64 {
  return math.Sqrt(x*x + y*y)
}

func area(x, y float64) float64 {
  return x * y / 2
}

func triangularPrism1(a float64, h float64) float64 {
  return a * h
}

func triangularPrism2(fn func(float64, float64) float64, h float64) float64 {
  return fn(3, 4) * h
}

func main() {
  fmt.Printf("%T\n", hypotenuse)
  fmt.Println("hypotenuse(3, 4):", hypotenuse(3, 4))
  fmt.Println("triangularPrism1:", triangularPrism1(area(3, 4), 10))
  fmt.Println("triangularPrism2:", triangularPrism2(area, 10))
}

// >>> func(float64, float64) float64
// >>> hypotenuse(3, 4): 5
// >>> triangularPrism1: 50
// >>> triangularPrism2: 50

実務でこんな簡単な計算をこんなに複雑化していたら上司にぶっ飛ばされると思いますが、とりあえず昔習った三平方の定理を使ったり三角柱の体積(Triangular prism)を求めたりしています。

出力結果からわかるように、変数(関数)hypotenuseの型情報はfunc(float64, float64) float64となっております。

つまり変数に名前のない関数を代入することで変数を関数として扱うことが出来るようになります。(JavaScriptでいうところの無名関数)

triangularPrism1とtriangularPrism2の違いは引数が面積の計算結果面積の計算式かというところです。

余談ですが、mathパッケージ内の関数sqrtはSquare rootの略で、平方根を求める関数です。

スコープとクロージャ(関数閉包)

クロージャ(closure)はどの言語でも共通でつまづく人が多く見受けられる概念で、ここではスコープの理解が必須となってくるので、頭に入れておくといいと思います。ちなみに「実務においての使いどころ」の紹介は長くなりそうでしたので、割愛しております。

変数のスコープはグローバルローカルが存在します。

といっても場合によって若干意味合いが変わってくるのですが、

例えば、

package main

import "fmt"

const Pi = 3.14
const Radius = 2

func area(r float64) float64 {
  return r * r * Pi
}

func main() {
  fmt.Println(area(Radius))
  fmt.Println(area(Radius))

  const Radius = 3
  fmt.Println(area(Radius))
}

// >>> 12.56
// >>> 12.56
// >>> 28.26

こちらは嫌ほど習ったであろう円の面積を求める関数を使ってますが、関数mainに着目すると、ブロック外のRadiusはグローバル、ブロック内のRadiusローカルということになります。

また、出力結果からわかるように、同一の定数・変数が定義されていた場合、優先されるのはローカルです。

次はこちら。

package main

import "fmt"

const Pi = 3.14

func area() float64 {
  return Radius * Radius * Pi
}

func main() {
  const Radius = 3
  fmt.Println(area())
}

これは実行できるでしょうか。

答えはNoです。

定数Radiusは関数areaのスコープ内には存在しないので、undefined: Radiusというようなエラーが返されるでしょう。

まとめるとこんな感じになります。

package main

import "fmt"

func add(x int) int {
  u := 2
  // 変数u, xの使用可能範囲
  return x * u
  // 変数u, xの使用可能範囲
}

func main() {
  r := 1
  // 変数rの使用可能範囲
  for i := 0; i < 10; i++ {
    // 変数iの使用可能範囲
    if v := r + 10; v < 30 {
      // 変数vの使用可能範囲
      fmt.Println(v)
      // 変数vの使用可能範囲
    }
    // 変数iの使用可能範囲
  }
  // 変数rの使用可能範囲
}

ここからですが、ではこれはどうなるでしょうか。

package main

import "fmt"

func call() func() {
  voice := "hello"
  return func() {
    fmt.Println(voice)
  }
}

func main() {
  person := call()
  person()
}

この変数personはちゃんと声を発してくれるでしょうか?

正解はYesです。

でも変数personの中身はfunc () { fmt.Println(voice) }のみのはずなのにってなりますよね。

これは関数が代入される時、レキシカルスコープというのが用いられていて、例えば今回のように代入される関数が知らない名前(ここでいうところのvoice)をスコープ内で見つけた時に、スコープ外を確認し、一致する名前を見つけ、それをレキシカル変数として保持する機能があります。

それらをうまく使ったのがクロージャです。

次をご覧ください。

package main

import "fmt"

func adder() func() int {
  i := 0
  return func() int {
    i++
    return i
  }
}

func main() {
  s := make([]int, 0)
  fn := adder()
  for i := 0; i < 10; i++ {
    s = append(s, fn())
  }
  fmt.Println(s)
}

// >>> [1 2 3 4 5 6 7 8 9 10]

A Tour of Goの改変です。

整数を返す関数を変数に渡しておいて、ループでスライスに突っ込んでいく、というシンプルな処理ですが、スライスに入っている値が回数に応じてインクリメントされているということが解ると思います。

これも先ほどと同様に変数(関数)fnには関数adder変数iが保持されており、それを返り値となる関数内で変更をすることで、毎回異なる値が返ってくるようになっています。

↑ついでにこれの問題を解いてみました。フィボナッチ久しぶり。

以下がソースです。

package main

import "fmt"

func fibonacci() func() int {
  p, q := 0, 0
  r := 1
  return func() int {
    p = q
    q = r
    r = p + q
    return p
  }
}

func main() {
  f := fibonacci()
  for i := 0; i < 10; i++ {
    fmt.Println(f())
  }
}

こういった数列も作れたりします。

説明ベタというのもあり、ピンとこない方もいるかと思いますが、誤解を恐れずにいうとクロージャとはリセットされない(状態を保持できる)変数を関数に持たせる使い方ということになります。

ある関数でしか使用しない変数を一つの関数に閉じ込めることができるので、余計なところに目を向けなくても良いというのもいいですね。

関数は使いこなせば本当に楽しくなっていろんなことをしたくなりますが、不具合のほとんどは関数に起因します

賢い関数を書けるのはものすごく憧れるけど、やっぱりシンプルが一番。