Rubyでの責任あるモンキーパッチ
2011年に初めてRubyコードを専門的に書き始めたとき、この言語について最も印象に残ったことの1つは、その柔軟性でした。 Rubyのように、すべてが可能であるように感じました。 C#やJavaなどの言語の硬直性と比較すると、Rubyプログラムは生きているように見えました。 。
Rubyプログラムでできる素晴らしいことがいくつあるか考えてみてください。メソッドは自由に定義および削除できます。存在しないメソッドを呼び出すことができます。あなたは薄い空気から無名のクラス全体を想起させることができます。絶対にワイルドです。
しかし、それで話は終わりではありません。これらの手法は独自のコード内で適用できますが、Rubyでは仮想マシンにロードされたものすべてに適用することもできます。つまり、自分のコードと同じくらい簡単に他の人のコードをいじることができます。
モンキーパッチとは何ですか?
モンキーパッチを入力します 。
要するに、monkeypatchesは既存のコードを「monkeywith」します。既存のコードは、gemやRuby標準ライブラリからのコードのように、直接アクセスできないコードであることがよくあります。パッチは通常、元のコードの動作を変更してバグを修正したり、パフォーマンスを向上させたりするように設計されています。
最も洗練されていないmonkeypatchesは、rubyクラスを再開し、メソッドを追加またはオーバーライドすることで動作を変更します。
この再開のアイデアは、Rubyのオブジェクトモデルの中核です。 Javaでは、クラスは1回しか定義できませんが、Rubyクラス(およびそのことについてはモジュール)は複数回定義できます。クラスを2回目、3回目、4回目などで定義すると、再開していると言います。 それ。定義した新しいメソッドはすべて既存のクラス定義に追加され、そのクラスのインスタンスで呼び出すことができます。
この短い例は、クラス再開の概念を示しています。
class Sounds
def honk
"Honk!"
end
end
class Sounds
def squeak
"Squeak!"
end
end
sounds = Sounds.new
sounds.honk # => "Honk!"
sounds.squeak # => "Squeak!"
両方の#honk
に注意してください および#squeak
メソッドはSounds
で利用できます 再開の魔法を通してクラス。
基本的に、モンキーパッチはサードパーティのコードでクラスを再開する行為です。
モンキーパッチは危険ですか?
前の文があなたを怖がらせたなら、それはおそらく良いことです。モンキーパッチは、特に不注意に行われた場合、本当の混乱を引き起こす可能性があります。
Array#<<
を再定義するとどうなるかを少し考えてみてください。 :
class Array
def <<(*args)
# do nothing 😈
end
end
これらの4行のコードにより、プログラム全体のすべての配列インスタンスが壊れています。
さらに、#<<
の元の実装 なくなっている。 Rubyプロセスを再起動する以外に、元に戻す方法はありません。
モンキーパッチがひどく間違っているとき
2011年に、私は著名なソーシャルネットワーキング会社で働いていました。当時、コードベースはRuby1.8.7で実行される大規模なRailsモノリスでした。毎日数百人のエンジニアがコードベースに貢献し、開発のペースは非常に速かった。
ある時点で、私のチームはString#%
をモンキーパッチすることにしました。 国際化の目的で複数形を書きやすくするため。パッチでできることの例を次に示します。
replacements = {
horse_count: 3,
horses: {
one: "is 1 horse",
other: "are %{horse_count} horses"
}
}
# "there are 3 horses in the barn"
"there %{horse_count:horses} in the barn" % replacements
パッチを作成し、最終的に本番環境にデプロイしました...それが機能しなかったことがわかりました。ユーザーには、リテラル%{...}
の文字列が表示されていました うまく複数形のテキストの代わりに文字。それは意味がありませんでした。このパッチは、私のラップトップの開発環境で完全に機能していました。なぜ本番環境で機能しなかったのですか?
当初、Ruby自体にバグが見つかったと思っていましたが、その後、本番のRailsコンソールが開発中のRailsコンソールとは異なる結果を生成することがわかりました。両方のコンソールが同じバージョンのRubyで実行されていたため、Ruby標準ライブラリのバグを除外できました。他に何かが起こっていました。
数日間頭を悩ませた後、同僚は、別のを追加したRails初期化子を追跡することができました。 String#%
の実装 私たちの誰も前に見たことがなかったこと。さらに複雑なことに、この以前の実装にもバグが含まれていたため、本番コンソールで見た結果はRubyの公式ドキュメントとは異なりました。
しかし、それで話は終わりではありません。以前のモンキーパッチを追跡したところ、他にも3つ以上のパッチが見つかり、すべて同じ方法でパッチを適用しました。 私たちは恐怖でお互いを見ました。これはどのように機能しましたか??
最終的に、Railsの熱心な読み込みまで一貫性のない動作をチョークしました。開発中、RailsはRubyファイルをレイジーロードします。つまり、require
の場合にのみロードします。 d。ただし、本番環境では、Railsは初期化時にアプリのすべてのRubyファイルをロードします。これにより、大きなモンキーレンチがモンキーパッチに投げ込まれる可能性があります。
クラスを再開した場合の結果
この場合、各モンキーパッチはString
を再開しました クラスを作成し、既存のバージョンの#%
を効果的に置き換えました 別のものとの方法。このアプローチにはいくつかの大きな落とし穴があります:
- 最後に適用されたパッチが「勝ち」ます。つまり、動作はロードの順序に依存します
- 元の実装にアクセスする方法はありません
- パッチは監査証跡をほとんど残さないため、後で見つけるのが非常に困難になります
当然のことながら、おそらく、これらすべてに遭遇しました。
最初は、他のモンキーパッチがプレイされていることすら知りませんでした。 winingメソッドのバグのため、元の実装が壊れているように見えました。他の競合するパッチを発見したとき、大量のputs
を追加せずにどちらが勝ったかを判断することは不可能でした。 ステートメント。
最後に、開発でどちらの方法が勝ったかを発見した場合でも、本番環境では別の方法が勝ちます。また、Ruby 1.8にはすばらしいMethod#source_location
がなかったため、どのパッチが最後に適用されたかをプログラムで判断することも困難でした。 私たちが今持っている方法。
何が起こっているのかを理解するために少なくとも1週間を費やしましたが、完全に回避可能な問題を追いかけるのに本質的に無駄な時間を費やしました。
最終的に、LocalizedString
を導入することにしました。 #%
を伴うラッパークラス 方法。 String
その後、モンキーパッチは単純に次のようになりました。
class String
def localize
LocalizedString.new(self)
end
end
モンキーパッチが失敗した場合
私の経験では、monkeypatchesは次の2つの理由のいずれかで失敗することがよくあります。
- パッチ自体が壊れています。 上記のコードベースでは、同じメソッドの競合する実装がいくつかあっただけでなく、「勝った」メソッドは機能しませんでした。
- 仮定は無効です。 ホストコードが更新され、パッチは記述どおりに適用されなくなりました。
2番目の箇条書きを詳しく見てみましょう。
ベストレイドプランでも...
モンキーパッチは、最初に到達したのと同じ理由で失敗することがよくあります—元のコードにアクセスできないためです。まさにその理由で、元のコードがあなたの下から変わる可能性があります。
アプリが依存しているgemのこの例を考えてみましょう:
class Sale
def initialize(amount, discount_pct, tax_rate = nil)
@amount = amount
@discount_pct = discount_pct
@tax_rate = tax_rate
end
def total
discounted_amount + sales_tax
end
private
def discounted_amount
@amount * (1 - @discount_pct)
end
def sales_tax
if @tax_rate
discounted_amount * @tax_rate
else
0
end
end
end
待って、そうではありません。消費税は、割引額ではなく、全額に適用する必要があります。プロジェクトにプルリクエストを送信します。メンテナがPRをマージするのを待っている間に、このモンキーパッチをアプリに追加します:
class Sale
private
def sales_tax
if @tax_rate
@amount * @tax_rate
else
0
end
end
end
それは完璧に動作します。チェックインして忘れてしまいます。
長い間、すべてが順調です。その後、ある日、財務チームから、会社が1か月間消費税を徴収していない理由を尋ねるメールが送信されます。
混乱して、あなたは問題を掘り下げ始め、最終的に同僚の1人がSale
を含むgemを最近更新したことに気付きました。 クラス。更新されたコードは次のとおりです:
class Sale
def initialize(amount, discount_pct, sales_tax_rate = nil)
@amount = amount
@discount_pct = discount_pct
@sales_tax_rate = sales_tax_rate
end
def total
discounted_amount + sales_tax
end
private
def discounted_amount
@amount * (1 - @discount_pct)
end
def sales_tax
if @sales_tax_rate
discounted_amount * @sales_tax_rate
else
0
end
end
end
プロジェクトメンテナの1人が@tax_rate
の名前を変更したようです @sales_tax_rate
へのインスタンス変数 。モンキーパッチは古い@tax_rate
の値をチェックします 変数。常にnil
。エラーが発生したことがないため、誰も気づきませんでした。アプリは何も起こらなかったかのように動きました。
なぜモンキーパッチなのか
これらの例を考えると、モンキーパッチは潜在的な頭痛の種に値しないように思えるかもしれません。では、なぜそれを行うのでしょうか。私の意見では、3つの主要なユースケースがあります:
- 壊れたまたは不完全なサードパーティコードを修正するには
- 開発における変更または複数の変更をすばやくテストするため
- 既存の機能をインストルメンテーションまたはアノテーションコードでラップする
場合によっては、のみ サードパーティのコードのバグやパフォーマンスの問題に対処する実行可能な方法は、モンキーパッチを適用することです。
しかし、大きな力には大きな責任が伴います。
責任を持ってモンキーパッチを適用
良いか悪いかではなく、責任を中心にモンキーパッチの会話を組み立てるのが好きです。確かに、モンキーパッチはうまく行かないと混乱を引き起こす可能性があります。ただし、ある程度の注意と注意を払って行われた場合、状況に応じてそれに手を伸ばすことを避ける理由はありません。
これが私が従おうとしているルールのリストです:
- わかりやすい名前のモジュールでパッチをラップし、
Module#prepend
を使用します 適用するには - 正しいものにパッチを適用していることを確認してください
- パッチの表面積を制限する
- 脱出用ハッチを用意してください
- 過剰なコミュニケーション
この記事の残りの部分では、これらのルールを使用して、RailsのDateTimeSelector
のモンキーパッチを作成します。 したがって、オプションで、破棄されたフィールドのレンダリングをスキップします。これは、私が数年前に実際にRailsに加えようとした変更です。詳細はこちらでご覧いただけます。
ただし、モンキーパッチを理解するために、破棄されたフィールドについて多くを知る必要はありません。結局のところ、それが行うのは、build_hidden
と呼ばれる単一のメソッドを置き換えることだけです。 効果的に何もしないもので。
始めましょう!
Module#prepend
を使用する
以前の役割で遭遇したコードベースでは、String#%
のすべての実装 String
を再度開くことで適用されました クラス。これが私が前に述べた欠点の拡張リストです:
- エラーは、パッチコードではなく、ホストクラスまたはモジュールから発生したようです
- パッチで定義するメソッドはすべて、既存のメソッドを同じ名前に置き換えます。つまり、元の実装を呼び出す方法はありません。
- どのパッチが適用されたか、したがってどのメソッドが「勝った」かを知る方法はありません
- パッチは監査証跡をほとんど残さないため、後で見つけるのが非常に困難になります
代わりに、パッチをモジュールにラップし、Module#prepend
を使用して適用することをお勧めします。 。そうすることで、元の実装を自由に呼び出すことができ、Module#ancestors
をすばやく呼び出すことができます。 継承階層にパッチが表示されるため、問題が発生した場合に簡単に見つけることができます。
最後に、単純なprepend
何らかの理由でパッチを無効にする必要がある場合は、ステートメントを簡単にコメントアウトできます。
Railsモンキーパッチのモジュールの始まりは次のとおりです。
module RenderDiscardedMonkeypatch
end
ActionView::Helpers::DateTimeSelector.prepend(
RenderDiscardedMonkeypatch
)
正しいものにパッチを当てる
この記事から1つ取り上げる場合は、次のようにします。正しいコードにパッチを適用していることがわかっている場合を除いて、モンキーパッチを適用しないでください。ほとんどの場合、あなたの仮定がまだ成り立っていることをプログラムで検証することが可能であるはずです(これは結局のところRubyです)。チェックリストは次のとおりです:
- パッチを適用しようとしているクラスまたはモジュールが存在することを確認してください
- メソッドが存在し、適切なアリティを持っていることを確認してください
- パッチを適用しているコードがgemに存在する場合は、gemのバージョンを確認してください
- 仮定が成り立たない場合は、役立つエラーメッセージを表示してベイルアウトします
すぐに、私たちのパッチコードはかなり重要な仮定をしました。 ActionView::Helpers::DateTimeSelector
という定数を想定しています。 存在し、クラスまたはモジュールです。
クラス/モジュールの確認
パッチを適用する前に、定数が存在することを確認しましょう:
module RenderDiscardedMonkeypatch
end
const = begin
Kernel.const_get('ActionView::Helpers::DateTimeSelector')
rescue NameError
end
if const
const.prepend(RenderDiscardedMonkeypatch)
end
すばらしいですが、ローカル変数(const
)がリークされました )グローバルスコープに。それを修正しましょう:
module RenderDiscardedMonkeypatch
def self.apply_patch
const = begin
Kernel.const_get('ActionView::Helpers::DateTimeSelector')
rescue NameError
end
if const
const.prepend(self)
end
end
end
RenderDiscardedMonkeypatch.apply_patch
チェック方法
次に、パッチを適用したbuild_hidden
を紹介しましょう。 方法。また、それが存在し、適切な数の引数を受け入れる(つまり、適切なアリティを持っている)ことを確認するためのチェックを追加しましょう。これらの仮定が当てはまらない場合は、おそらく何かが間違っています:
module RenderDiscardedMonkeypatch
class << self
def apply_patch
const = find_const
mtd = find_method(const)
if const && mtd && mtd.arity == 2
const.prepend(self)
end
end
private
def find_const
Kernel.const_get('ActionView::Helpers::DateTimeSelector')
rescue NameError
end
def find_method(const)
return unless const
const.instance_method(:build_hidden)
rescue NameError
end
end
def build_hidden(type, value)
''
end
end
RenderDiscardedMonkeypatch.apply_patch
Gemのバージョンを確認する
最後に、正しいバージョンのRailsを使用していることを確認しましょう。 Railsがアップグレードされた場合、パッチも更新する必要があるかもしれません(または完全に削除する必要があります)。
module RenderDiscardedMonkeypatch
class << self
def apply_patch
const = find_const
mtd = find_method(const)
if const && mtd && mtd.arity == 2 && rails_version_ok?
const.prepend(self)
end
end
private
def find_const
Kernel.const_get('ActionView::Helpers::DateTimeSelector')
rescue NameError
end
def find_method(const)
return unless const
const.instance_method(:build_hidden)
rescue NameError
end
def rails_version_ok?
Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR == 1
end
end
def build_hidden(type, value)
''
end
end
RenderDiscardedMonkeypatch.apply_patch
助けを借りて救済する
検証コードで期待と現実の不一致が明らかになった場合は、エラーを発生させるか、少なくとも役立つ警告メッセージを出力することをお勧めします。ここでの考え方は、何かがおかしいと思われるときにあなたとあなたの同僚に警告することです。
Railsパッチを変更する方法は次のとおりです。
module RenderDiscardedMonkeypatch
class << self
def apply_patch
const = find_const
mtd = find_method(const)
unless const && mtd && mtd.arity == 2
raise "Could not find class or method when patching "\
"ActionView's date_select helper. Please investigate."
end
unless rails_version_ok?
puts "WARNING: It looks like Rails has been upgraded since "\
"ActionView's date_select helper was monkeypatched in "\
"#{__FILE__}. Please reevaluate the patch."
end
const.prepend(self)
end
private
def find_const
Kernel.const_get('ActionView::Helpers::DateTimeSelector')
rescue NameError
end
def find_method(const)
return unless const
const.instance_method(:build_hidden)
rescue NameError
end
def rails_version_ok?
Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR == 1
end
end
def build_hidden(type, value)
''
end
end
RenderDiscardedMonkeypatch.apply_patch
表面積の制限
モンキーパッチでヘルパーメソッドを定義することは完全に無害に見えるかもしれませんが、Module#prepend
を介して定義されたメソッドはすべて覚えておいてください。 継承の魔法を通して既存のものを上書きします。ホストクラスまたはモジュールが特定のメソッドを定義していないように見えるかもしれませんが、確実に知ることは困難です。このため、パッチを適用するメソッドのみを定義しようとしています。
これは、オブジェクトのシングルトンクラスで定義されたメソッド、つまりclass << self
内で定義されたメソッドにも適用されることに注意してください。 。
Railsパッチを変更して1つの#build_hidden
のみを置き換える方法は次のとおりです。 方法:
module RenderDiscardedMonkeypatch
class << self
def apply_patch
const = find_const
mtd = find_method(const)
unless const && mtd && mtd.arity == 2
raise "Could not find class or method when patching"\
"ActionView's date_select helper. Please investigate."
end
unless rails_version_ok?
puts "WARNING: It looks like Rails has been upgraded since"\
"ActionView's date_selet helper was monkeypatched in "\
"#{__FILE__}. Please reevaluate the patch."
end
const.prepend(InstanceMethods)
end
private
def find_const
Kernel.const_get('ActionView::Helpers::DateTimeSelector')
rescue NameError
end
def find_method(const)
return unless const
const.instance_method(:build_hidden)
rescue NameError
end
def rails_version_ok?
Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR == 1
end
end
module InstanceMethods
def build_hidden(type, value)
''
end
end
end
RenderDiscardedMonkeypatch.apply_patch
脱出用ハッチを自分に与える
可能であれば、モンキーパッチの機能をオプトインするのが好きです。これは、パッチが適用されたコードが呼び出される場所を制御できる場合にのみ、実際にオプションになります。 Railsパッチの場合、@options
を介して実行できます。 DateTimeSelector
のハッシュ :
module RenderDiscardedMonkeypatch
class << self
def apply_patch
const = find_const
mtd = find_method(const)
unless const && mtd && mtd.arity == 2
raise "Could not find class or method when patching"\
"ActionView's date_select helper. Please investigate."
end
unless rails_version_ok?
puts "WARNING: It looks like Rails has been upgraded since"\
"ActionView's date_selet helper was monkeypatched in "\
"#{__FILE__}. Please reevaluate the patch."
end
const.prepend(InstanceMethods)
end
private
def find_const
Kernel.const_get('ActionView::Helpers::DateTimeSelector')
rescue NameError
end
def find_method(const)
return unless const
const.instance_method(:build_hidden)
rescue NameError
end
def rails_version_ok?
Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR == 1
end
end
module InstanceMethods
def build_hidden(type, value)
if @options.fetch(:render_discarded, true)
super
else
''
end
end
end
end
RenderDiscardedMonkeypatch.apply_patch
良い!これで、発信者はdate_select
を呼び出すことでオプトインできます 新しいオプションのヘルパー。他のコードパスは影響を受けません:
date_select(@user, :date_of_birth, {
order: [:month, :day],
render_discarded: false
})
過剰なコミュニケーション
私があなたに持っている最後のアドバイスはおそらく最も重要です—あなたのパッチが何をするのか、そしていつそれを再検討する時なのかを伝えることです。モンキーパッチの目標は、常に最終的にパッチを完全に削除することです。そのために、責任あるモンキーパッチには次のようなコメントが含まれています。
- パッチの機能を説明してください
- パッチが必要な理由を説明してください
- パッチの前提条件の概要
- チームが更新された宝石を引き込むなど、代替ソリューションを再検討する必要がある将来の日付を指定します
- 関連するプルリクエスト、ブログ投稿、StackOverflowの回答などへのリンクを含めます。
警告を印刷したり、事前に決められた日にテストに失敗したりして、パッチの前提条件を再確認し、それがまだ必要かどうかを検討するようにチームに促すこともできます。
これがRailsの最終バージョンですdate_select
パッチ、コメントと日付チェックを完備:
# ActionView's date_select helper provides the option to "discard" certain
# fields. Discarded fields are (confusingly) still rendered to the page
# using hidden inputs, i.e. <input type="hidden" />. This patch adds an
# additional option to the date_select helper that allows the caller to
# skip rendering the chosen fields altogether. For example, to render all
# but the year field, you might have this in one of your views:
#
# date_select(:date_of_birth, order: [:month, :day])
#
# or, equivalently:
#
# date_select(:date_of_birth, discard_year: true)
#
# To avoid rendering the year field altogether, set :render_discarded to
# false:
#
# date_select(:date_of_birth, discard_year: true, render_discarded: false)
#
# This patch assumes the #build_hidden method exists on
# ActionView::Helpers::DateTimeSelector and accepts two arguments.
#
module RenderDiscardedMonkeypatch
class << self
EXPIRATION_DATE = Date.new(2021, 8, 15)
def apply_patch
if Date.today > EXPIRATION_DATE
puts "WARNING: Please re-evaluate whether or not the ActionView "\
"date_select patch present in #{__FILE__} is still necessary."
end
const = find_const
mtd = find_method(const)
# make sure the class we want to patch exists;
# make sure the #build_hidden method exists and accepts exactly
# two arguments
unless const && mtd && mtd.arity == 2
raise "Could not find class or method when patching "\
"ActionView's date_select helper. Please investigate."
end
# if rails has been upgraded, make sure this patch is still
# necessary
unless rails_version_ok?
puts "WARNING: It looks like Rails has been upgraded since "\
"ActionView's date_select helper was monkeypatched in "\
"#{__FILE__}. Please re-evaluate the patch."
end
# actually apply the patch
const.prepend(InstanceMethods)
end
private
def find_const
Kernel.const_get('ActionView::Helpers::DateTimeSelector')
rescue NameError
# return nil if the constant doesn't exist
end
def find_method(const)
return unless const
const.instance_method(:build_hidden)
rescue NameError
# return nil if the method doesn't exist
end
def rails_version_ok?
Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR == 1
end
end
module InstanceMethods
# :render_discarded is an additional option you can pass to the
# date_select helper in your views. Use it to avoid rendering
# "discarded" fields, i.e. fields marked as discarded or simply
# not included in date_select's :order array. For example,
# specifying order: [:day, :month] will cause the helper to
# "discard" the :year field. Discarding a field renders it as a
# hidden input. Set :render_discarded to false to avoid rendering
# it altogether.
def build_hidden(type, value)
if @options.fetch(:render_discarded, true)
super
else
''
end
end
end
end
RenderDiscardedMonkeypatch.apply_patch
結論
上で概説した提案のいくつかはやり過ぎのように思えるかもしれません。 Railsパッチには、実際のパッチコードよりもはるかに防御的な検証コードが含まれています!
その余分なコードはすべて、ブロードソードの鞘と考えてください。保護層に包まれていると、切断を回避するのがはるかに簡単になります。
しかし、本当に重要なのは、責任あるモンキーパッチを本番環境に展開することに自信を持っていることです。無責任なものは、あなたやあなたの会社の時間、お金、そして開発者の健康を犠牲にするのを待っている単なる時限爆弾です。
P.S。 Ruby Magicの投稿をマスコミから離れたらすぐに読みたい場合は、Ruby Magicニュースレターを購読して、投稿を1つも見逃さないでください。
-
Rubyでの挿入ソートを理解する
注:これは、Rubyを使用したさまざまなソートアルゴリズムの実装を検討するシリーズのパート4です。パート1ではバブルソート、パート2では選択ソート、パート3ではマージソートについて説明しました。 データを並べ替えるためのさまざまな方法を引き続き検討するため、挿入並べ替えに目を向けます。挿入ソートが好きな理由はたくさんあります!まず、挿入ソートは安定です。 、これは、等しいキーを持つ要素の相対的な順序を変更しないことを意味します。 インプレースアルゴリズムでもあります 、は、並べ替えられた要素を格納するための新しい配列を作成しないことを意味します。最後に、挿入ソートは、すぐにわかるように、実
-
Ruby2.6の9つの新機能
Rubyの新しいバージョンには、新しい機能とパフォーマンスの改善が含まれています。 変更についていきますか? 見てみましょう! 無限の範囲 Ruby 2.5以前のバージョンは、すでに1つの形式の無限範囲をサポートしています( Float ::INFINITY を使用) )、しかしRuby2.6はこれを次のレベルに引き上げます。 新しい無限の範囲 次のようになります: (1..) これは、(1..10)のような終了値がないため、通常の範囲とは異なります。 。 使用例 : [a, b, c].zip(1..) # [[a, 1], [b, 2], [c, 3]] [1,2,3,