95
40

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

プログラミング教材の模範解答が模範的でない件

Posted at

Qiita の新着記事を眺めていると,ときどき同じお題に取り組んだ結果を書いたものをいくつも目にすることがあります。
「模範解答」があったりするので,おそらく何らかの教材に取り組まれたのだろうと思います。
その「模範解答」のコードを見て首を傾げることがしばしばあったので,本記事で具体的に指摘してみたいと思います。
取り上げるのはすべて Ruby のものです。

たいがい出典が書かれていないので,原典を参照することができないのですが。

※本記事は,そういった記事および著者を批判する意図を微塵も持っていません。むしろ応援したい気持ちで書いています。
※また,取り上げた教材およびその提供者を侮辱したり非難したりするつもりもありません。不遜かもしれませんが改善に役立てば幸いです。

なお,教材によってはお題そのものがおかしかったりしますが,今回は取り上げません。

今回,三つのお題を取り上げました。引用したのは,それぞれのお題について,見つけることのできた最も新しい記事です。

例:Ruby の each の入れ子

お題は

fruits_price = [["apple", [200, 250, 220]], ["orange", [100, 120, 80]], ["melon", [1200, 1500]]] 

という形で果物の価格リストが与えられていて

appleの合計金額は670円です
orangeの合計金額は300円です
melonの合計金額は2700円です

という出力を得る,というもの。
Qiita 上で tag:ruby fruits_price で検索すると 30 件以上の記事がヒットします。

模範解答は,@yu-ki87 さんの記事 によれば

fruits_price = [["apple", [200, 250, 220]], ["orange", [100, 120, 80]], ["melon", [1200, 1500]]]

fruits_price.each do |fruit|
  sum = 0
  fruit[1].each do |price|
    sum += price
  end
  puts "#{fruit[0]}の合計金額は#{sum}円です"
end

とのことです(原典が分からないので記事から引用することをお許しください;以下同じ)。

合計を計算するのに

sum = 0
fruit[1].each do |price|
  sum += price
end

としていますが,Ruby で配列の数値の総和を求めるのにこんな面倒なコードを書くことはなく,

sum = fruit[1].sum

で事足ります。
(引用した記事でも記事主さんは sum をお使いになっています。模範解答が複雑すぎることに戸迷われたのではないでしょうか)

これはおそらく教材が作られたのがかなり昔なのでしょう。
Array#sum が導入されたのは 2016 年 12 月にリリースされた Ruby 2.4.0 です。(本記事投稿時点で)もうすぐ 6 年になろうとしています。
Array#sum が使えなかった Ruby 2.3 系の公式サポートが終了したのは 2019 年 3 月。つまり,3 年前には「使ってはいけないバージョン」になっていたのです。

この教材は,おそらく each の入れ子に慣れさせるためのものだと思うので,sum を使ってしまっては入れ子になりませんよね。それなら

apple
  200円
  250円
  220円
orange
  100円
  120円
  80円
melon
  1200円
  1500円

と表示させるお題でよかったのでは,という気がします。

それから,

fruits_price.each do |fruit|

end

のブロック内で fruit[0]fruit[1] を使っていますが,こういうスタイルは初心者に真似してほしくないところです。
ブロックパラメーターを二つにして

fruits_price.each do |fruit_name, fruit_prices|

end

と書くべきでしょう。
(ブロックパラーメーター名は単に nameprices でもいいかと思います)

「初心者にはブロックパラメーターへの多重代入は難しい」という配慮のもと,あえて単一のブロックパラメーターにしたのかもしれません。
それはそれで理解できるのですが,模範解答を 2 段階で提示するとか,丁寧な解説を付けるといった方法もあったのでは,という気がします。
(原典を見ていないので,何か誤解があるかもしれませんが)

例:クラスとインスタンスの概念を用いたコード

@fujitacoma さんの記事 によれば,お題は,

class Article

  def initialize(author, title, content)
    @author = author
    @title = title
    @content = content
  end

end

というクラスが与えられ,ここにコードを書き足して

著者: 阿部
タイトル: Rubyの素晴らしさについて
本文: Awesome Ruby!

という表示を得る,というもの。
これも検索するといくつも記事が出てくるので,どこかに教材があるのでしょう。

模範解答は

class Article

  def initialize(author, title, content)
    @author = author
    @title = title
    @content = content
  end

  def author
    @author
  end

  def title
    @title
  end

  def content
    @content
  end

end

article = Article.new("阿部", "Rubyの素晴らしさについて", "Awesome Ruby!")
puts "著者: #{article.author}"
puts "タイトル: #{article.title}"
puts "本文: #{article.content}"

だそうです。

ゲッターメソッド(インスタンス変数の値を読み出すメソッド)三つを真面目に定義していますが,attr_reader を使えば

attr_reader :author, :title, :content

だけで済みます。
こうしなかったのは,「これにはまだ早い」学習者向けだったからかもしれません。それなら理解できます。
この点については,適切な誘導(まずマジメに定義することを学び,次に attr_reader で楽をすることを学ぶ)があるならそれでいいのでは,と思います。

それ以上に気になったのが,目的の出力を得るために,いちいちゲッターメソッドを一つ一つ呼び出しているところです。
このお題はおそらく「論文情報をオブジェクトで表現し,それを表示する」ということでしょう。
著者やタイトルをいちいち呼び出していたのではクラスを使う意味があまりないと思います。

論文情報を扱うクラスを作るのであれば,

class Article
  def initialize(author, title, content)
    @author = author
    @title = title
    @content = content
  end

  def show
    puts "著者: #{@author}"
    puts "タイトル: #{@title}"
    puts "本文: #{@content}"
  end
end

article = Article.new("阿部", "Rubyの素晴らしさについて", "Awesome Ruby!")
article.show

のように,全情報の表示を担うメソッドをクラスに持たせるべきではないでしょうか。

例:与えられた文字列の末尾の 2 文字を 3 回繰り返した文字列を出力

@itosyo4126 さんの記事 によれば,お題は

def extra_end(str)
  # 処理を記述
end

# 呼び出し例
extra_end('Hello')

と書いて,lololo を表示させるということのようです。

模範解答は

def extra_end(str)
  char_num = str.length
  right2 = str.slice(char_num - 2, 2)
  puts right2 * 3
end

とのこと(引用した記事では「模範解答」とは書かれていませんが,他の方の記事で同じコードが模範解答として挙がっていました)。

String#slice の正しい使い方の例ではありますが,インデックスに負数を与えれば後ろから数えた位置を与えるので,

def extra_end(str)
  right2 = str.slice(-2, 2)
  puts right2 * 3
end

とすればいいのではないでしょうか。

str.slice(-2, 2) はもっと簡潔に str[-2, 2] とも書けるので,

def extra_end(str)
  puts str[-2, 2] * 3
end

でよさそうに思います。

「末尾 2 文字」を「末尾 3 文字」に変更したりする可能性があるなら,変更箇所が二つある上記コードよりも,

def extra_end(str)
  puts str[-2..-1] * 3
end

のほうがよいかもしれません。
Ruby 2.6(2018 年 12 月リリース;公式サポートは既に終了)で導入された「終端無し Range」を使えば

def extra_end(str)
  puts str[-2..] * 3
end

と書けます。

String#slice およびエイリアスの関係にある String#[] にはさまざまな用法があり,初心者のうちからいっぺんに全部把握しようとするのは確かに難しいでしょう。
しかし,これも分かりやすい用法から始めて順に誘導するようになっているとよさそうに思います。

おわりに

教材が良くなっていってくれるとうれしいです。

95
40
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
95
40

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?