ほとんどのプログラミング言語には、関数を宣言するための特別なキーワードまたは構文があるのはなぜですか?[閉まっている]


39

ほとんどのプログラミング言語(動的および静的に型付けされた言語の両方)には、関数を宣言するための変数の宣言とは大きく異なる特別なキーワードおよび/または構文があります。関数は別の名前付きエンティティを宣言するのと同じように見えます:

たとえば、Pythonの場合:

x = 2
y = addOne(x)
def addOne(number): 
  return number + 1

何故なの:

x = 2
y = addOne(x)
addOne = (number) => 
  return number + 1

同様に、Javaのような言語では:

int x = 2;
int y = addOne(x);

int addOne(int x) {
  return x + 1;
}

何故なの:

int x = 2;
int y = addOne(x);
(int => int) addOne = (x) => {
  return x + 1;
}

この構文は、何か(関数または変数)を宣言するより自然な方法であり、一部の言語のように、defまたはfunctionいくつかの言語でキーワードを1つ少なくします。そして、IMO、それはより一貫性があり(変数または関数の型を理解するために同じ場所を見る)、おそらくパーサー/文法を書くことを少し簡単にします。

このアイデアを使用する言語はほとんどありませんが(CoffeeScript、Haskell)、ほとんどの一般的な言語には関数(Java、C ++、Python、JavaScript、C#、PHP、Ruby)の特別な構文があります。

両方の方法をサポートする(および型推論を行う)Scalaでも、次のように記述するのが一般的です。

def addOne(x: Int) = x + 1

のではなく:

val addOne = (x: Int) => x + 1

IMO、少なくともScalaで、これはおそらく最も簡単に理解できるバージョンですが、このイディオムはめったに続きません:

val x: Int = 1
val y: Int = addOne(x)
val addOne: (Int => Int) = x => x + 1

私は自分のおもちゃの言語に取り組んでいますが、そのような方法で言語を設計する場合に落とし穴があり、歴史的または技術的な理由がこのパターンに従っていない場合は疑問がありますか?


9
その理由はおそらく歴史的なものです。最初にそれをした人は誰でもそのようにして、他のみんながコピーしました。しかし、私たちが確実に知ることができるとは思わない。

29
これは、関数またはメソッドが単に別の名前付きエンティティではないためだと思います。関数型言語では(それらを渡すことができます)が、他の言語(Javaなど)では、関数またはメソッドは単純な変数とはまったく異なるものであり、そのように扱うことはできません(Java 8はこれを弱めることを認めますステートメント)、したがって、異なる動作をするため、関数/メソッドを異なる方法で定義するのが理にかなっています。
11684 14

15
あなたの提案が関数を宣言するより自然な方法であることに同意しません。私はCoffeescriptの構文が本当に嫌いで、その一部は関数宣言の構文に関係しています。事実、壊れていない場合は修正しないでください。「def」または「関数」を書くことは大したことではなく、考えられないか疲れた目で簡単に他の何かと間違われる可能性のあるいくつかの派手なPerlのようなシンボルと比べて一目見ただけで明らかです。完全に個人的なメモでは、例で提案されている構文は、現在の構文よりもいように見えます。
ロイ14

22
「=>」は「関数を宣言するための特別なキーワードまたは構文」ではないのはなぜですか?
TML 14

11
私はあなたについて知っているが、私にはしていない(int => int) addOne = (x) => {多くの「特別」と「複雑」よりint addOne(int) {...
ボグダンアレクサンドル

回答:


48

その理由は、最も一般的な言語は、関数型言語とその根源であるラムダ計算とは対照的に、Cファミリーの言語に由来するか、影響を受けているためだと思います。

そして、これらの言語では、関数は単なる別の値ではありません

  • C ++、C#、およびJavaでは、関数をオーバーロードできます。同じ名前で署名が異なる2つの関数を使用できます。
  • C、C ++、C#、およびJavaでは、関数を表す値を使用できますが、関数ポインター、ファンクター、デリゲート、および機能インターフェイスはすべて、関数自体とは異なります。理由の一部は、それらのほとんどが実際には単なる関数ではなく、何らかの(可変)状態を持つ関数であるということです。
  • 変数は(あなたが使用する必要があり、デフォルトで変更可能ですconstreadonlyまたはfinal変異を禁止する)が、機能が再割り当てすることはできません。
  • より技術的な観点からは、コード(関数で構成される)とデータは分離されています。通常、これらはメモリのさまざまな部分を占有し、アクセス方法も異なります。コードは一度読み込まれてから実行されるだけです(読み取りまたは書き込みは行われません)

    また、Cは「金属に近い」ことを意図していたため、言語の構文でもこの区別を反映することは理にかなっています。

  • C ++、C#、Java(2011、2007、2014)でラムダが最近導入されたことから明らかなように、関数プログラミングの基礎を形成する「関数は単なる価値」アプローチは、ごく最近になって共通言語で注目を集めています。


12
C#には、匿名関数(型推論と不格好な構文を持たない単なるラムダ)が2005
Eric Lippert 14

2
「より技術的な観点から、コード(関数で構成される)とデータは分離されています」-一部の言語、最もよく知っているように、LISPはその分離を行わず、実際にコードとデータをあまり扱いません。(LISPは最もよく知られた例ですが、これを行うREBOLのような他の言語の束があります)
ベンジャミングリュンバウム14

1
@BenjaminGruenbaum言語レベルではなく、メモリ内のコンパイル済みコードについて話していました。
svick 14

Cには、少なくともわずかに別の値と見なすことができる関数ポインターがあります。確かに混乱させる危険な値ですが、見知らぬことが起こりました。
パトリックヒューズ14

@PatrickHughesええ、2番目のポイントでそれらについて言及します。ただし、関数ポインターは関数とはまったく異なります。
svick 14

59

これは、機能が単なる「別の名前付きエンティティ」ではないことを人間が認識することが重要だからです。それらをそのように操作することが理にかなっている場合もありますが、それでも一目で認識できます。

理解できない文字の塊は機械が解釈するのには問題ないので、コンピューターが構文についてどう考えるかは問題ではありませんが、人間が理解して維持することはほとんど不可能です。

それは、すべてが最終的に比較およびジャンプ命令に要約されたとしても、whileおよびforループ、switch、if elseなどがある理由と同じ理由です。その理由は、コードを維持および理解する人間の利益のためにあるからです。

関数を「別の名前付きエンティティ」として提案している方法で使用すると、コードが見にくくなり、理解しにくくなります。


12
関数を名前付きエンティティとして扱うと、必然的にコードが理解しにくくなることに同意しません。これは、手続き型パラダイムに従うコードにはほぼ確実に当てはまりますが、機能的パラダイムに従うコードには当てはまらない場合があります。
カイルストランド14

29
すべてが最終的に比較およびジャンプ命令に要約されているもかかわらず、whileおよびforループ、switch、if elseなどがある理由と実際には同じ理由です」+1 すべてのプログラマーが機械言語に慣れていれば、これらすべての派手なプログラミング言語(高または低レベル)は必要ありません。しかし、真実は次のとおりです。誰もが、コードを書きたいマシンにどれだけ近いかに関して、異なる快適レベルを持っています。レベルを見つけたら、与えられた構文をそのまま使用する必要があります(実際に気になる場合は、独自の言語仕様とコンパイラを作成します)。
ホキ14

12
+1。より簡潔なバージョンは、「すべての自然言語が名詞と動詞を区別する理由」です。
msw 14

2
「理解できない文字の塊は機械が解釈するのには問題ありませんが、人間が理解して維持することはほとんど不可能です」質問で提案されているこのようなささいな構文変更に対するなんとかした資格。すぐに構文に適応します。この提案は、オブジェクト指向メソッドの呼び出し構文object.method(args)が当時だったよりも、慣例からの根本的な逸脱ではありませんが、後者は人間が理解して維持することが不可能ではないことが証明されています。ほとんどのオブジェクト指向言語では、メソッドはクラスインスタンスに格納されていると考えるべきではないという事実にもかかわらず。
マークヴァンレーウェン14

4
@marcvanLeeuwen。あなたのコメントは真実であり、理にかなっていますが、元の投稿が編集される方法によって多少の混乱が引き起こされます。実際には2つの質問があります:(i)なぜこのようになっているのですか?(ii)なぜこのようにしないのですか?。による答えwhatsisnameは、最初のポイントにもっと対処することであり(そしてこれらのフェイルセーフを削除する危険性について警告することです)、あなたのコメントは質問の2番目の部分により関連しています。実際、この構文を変更することは可能です(そして、あなたが説明したように、すでに何度も行われています...)が、すべての人に適しているわけではありません(oopも皆に適していないため。)
Hoki

10

先史時代に遡って、ALGOL 68と呼ばれる言語があなたが提案したものに近い構文を使用していたことを学ぶことに興味があるかもしれません。関数識別子は他の識別子と同様に値にバインドされていることを認識し、その言語で構文を使用して関数(定数)を宣言できます

function-type name =(parameter-listresult-typebody ;

具体的にはあなたの例は次のようになります

PROC (INT)INT add one = (INT n) INT: n+1;

宣言のRHSから初期型を読み取ることができるという冗長性を認識し、関数型は常にで始まりPROC、これは(通常は)

PROC add one = (INT n) INT: n+1;

ただし、=まだパラメータリストの前にあることに注意してください。また、関数変数(同じ関数型の別の値を後で割り当てることができる)が=必要な場合は、に置き換えて:=

PROC (INT)INT func var := (INT n) INT: n+1;
PROC func var := (INT n) INT: n+1;

ただし、この場合、両方の形式は実際には略語です。識別子func varはローカルで生成された関数への参照を指定するため、完全に展開されたフォームは

REF PROC (INT)INT func var = LOC PROC (INT)INT := (INT n) INT: n+1;

この特定の構文形式は簡単に慣れることができますが、明らかに他のプログラミング言語では大きな支持を得ていませんでした。Haskellのような関数型プログラミング言語でさえ、パラメーターリストに従うスタイルf n = n+1を好みます。その理由は主に心理的なものだと思います。すべての偶数の数学者が、多くの場合、私がそうであるように、好まないの後にFを = Nのn + 1を介してFN)= N + 1。=

ところで、上記の議論は変数と関数の重要な違いを強調しています:関数定義は通常、後で変更できない特定の関数値に名前をバインドしますが、変数定義は通常初期値を持つ識別子を導入しますが、後で変更できます。(絶対的な規則ではありません。関数変数と非関数定数はほとんどの言語で発生します。)さらに、コンパイルされた言語では、関数定義でバインドされた値は通常コンパイル時定数であるため、関数の呼び出しはコード内の固定アドレスを使用してコンパイルされます。C / C ++では、これも要件です。ALGOL 68に相当

PROC (REAL) REAL f = IF mood=sunny THEN sin ELSE cos FI;

関数ポインタを導入せずにC ++で記述することはできません。この種の特定の制限は、関数定義に異なる構文を使用することを正当化します。しかし、それらは言語のセマンティクスに依存しており、正当化はすべての言語に適用されるわけではありません。


1
"数学者は好まないfは = Nのn +オーバー1 FN)= Nだけ書くのが好き物理学者、もちろんのこと... + 1" fは = N + 1 ...
leftaroundabout

1
@leftaroundaboutそれは⟼が書くのが苦手だからです。正直に。
ピエールアラード14

8

例としてJavaとScalaについて言及しました。ただし、重要な事実を見落としていました。これらは関数ではなく、メソッドです。メソッドと関数は根本的に異なります。関数はオブジェクトであり、メソッドはオブジェクトに属します。

関数とメソッドの両方を備えたScalaでは、メソッドと関数の間に次の違いがあります。

  • メソッドはジェネリックにすることができますが、関数はできません
  • メソッドには、1つまたは複数のパラメーターリストを含めることはできませんが、関数には常に1つのパラメーターリストしかありません
  • メソッドには暗黙的なパラメーターリストを含めることができますが、関数にはできません
  • メソッドはデフォルトの引数を持つオプションのパラメータを持つことができますが、関数はできません
  • メソッドは繰り返しパラメータを持つことができますが、関数はできません
  • メソッドは名前によるパラメータを持つことができますが、関数はできません
  • メソッドは名前付き引数で呼び出すことができますが、関数はできません
  • 関数はオブジェクトであり、メソッドはそうではありません

そのため、少なくともこれらの場合、提案された交換は機能しません。


2
「関数はオブジェクトであり、メソッドはオブジェクトに属します。」これはすべての言語(C ++など)に適用されるはずですか?
svick

9
「メソッドには名前でパラメータを指定できますが、関数には指定できません」-これはメソッドと関数の基本的な違いではなく、単なるScalaの癖です。他のほとんど(すべて?)と同じです。
user253751 14

1
メソッドは基本的には特別な種類の関数です。-1。Java(および拡張Scala)には、他の種類の関数はありません。「関数」とは、1つのメソッドを持つオブジェクトのことです(一般的に、関数はオブジェクトやファーストクラスの値でさえない場合があります。それらは言語に依存します)。
Jan Hudec 14

@JanHudec:意味的に、Javaには関数とメソッドの両方があります。関数を参照するときに「静的メソッド」という用語を使用しますが、そのような用語はセマンティックの区別を消去しません。
supercat

3

私が考えることができる理由は次のとおりです。

  • コンパイラーが宣言する内容を知るのは簡単です。
  • これが関数であるか変数であるかを(些細な方法で)知ることが重要です。関数は通常ブラックボックスであり、内部実装については気にしません。私は、Scalaでの戻り型の型推論が嫌いです。なぜなら、戻り型を持つ関数を使用する方が簡単だと思うからです。
  • そして最も重要なのは、プログラミング言語の設計に使用されるクラウド戦略に従うことです。C ++はCプログラマを盗むために作成され、JavaはC ++プログラマを怖がらせない方法で設計され、C#はJavaプログラマを引き付けるために設計されました。私が思うにC#でさえ、驚くべきチームが背後にある非常に現代的な言語だと思いますが、JavaやCからもいくつかの間違いをコピーしました。

2
「…そしてJavaプログラマーを引き付けるC#」—これは疑問です。C#はJavaよりもC ++にはるかに似ています。時には、意図的にJavaと互換性がないように見えます(ワイルドカードも、内部クラスも、戻り値の型の共分散もありません)
Sarge Borsch 14

1
@SargeBorsch意図的にJavaとの互換性が失われたとは思わない。C#チームは単にそれを正しく実行しようとしたか、より良く実行しようとしたに違いない。Microsoftはすでに独自のJavaとJVMを作成し、訴えられました。そのため、C#チームはJavaを日食にしようと非常に動機付けられたようです。それは確かに互換性がなく、より良いです。Javaはいくつかの基本的な制限から始まりました。C#チームが、たとえばジェネリックを異なる方法(砂糖だけでなく型システムで表示可能)にすることを選んだことを嬉しく思います。
コデンハイム14

2

質問を振り返ると、極端にRAMが制約されているマシンでソースコードを編集したり、フロッピーディスクから読み取る時間を最小限にしようとすることに興味がない場合、キーワードを使用すると何が問題になりますか?

確かにを読むx=y+zよりも読みやすいですstore the value of y plus z into xが、それは句読点文字が本質的にキーワードよりも「良い」という意味ではありません。変数の場合はij、とkしているInteger、とxされReal、Pascalで次の行を考慮してください。

k := i div j;
x := i/j;

最初の行は切り捨て整数除算を実行し、2行目は実数除算を実行します。Pascalはdiv、既に別の目的(実数の除算)がある句読点を使用するのではなく、切り捨て整数除算演算子として使用しているため、うまく区別できます。

関数定義を簡潔にするのに役立ついくつかのコンテキストがありますが(たとえば、別の式の一部として使用されるラムダ)、関数は一般に際立っており、関数として簡単に視覚的に認識できるはずです。区別をはるかに微妙にし、句読点文字のみを使用することも可能かもしれませんが、ポイントは何でしょうか?言うFunction Foo(A,B: Integer; C: Real): Stringことで、関数の名前、期待されるパラメーター、および返されるものが明確になります。Functionいくつかの句読点文字に置き換えることで6文字または7文字短くすることもできますが、何が得られるでしょうか?

別の注意点は、ほとんどのフレームワークでは、特定のメソッドまたは特定の仮想バインディングに常に名前を関連付ける宣言と、特定のメソッドまたはバインディングを最初に識別する変数を作成する宣言との間に根本的な違いがあることですが、実行時に変更して別のものを識別することができます。これらはほとんどの手続き型フレームワークで非常に意味的に非常に異なる概念であるため、異なる構文を持つ必要があることは理にかなっています。


3
この質問は簡潔さに関するものではないと思います。特に、たとえばvoid f() {}実際にはC ++(auto f = [](){};)、C#(Action f = () => {};)およびJava(Runnable f = () -> {};)の同等のラムダよりも短いためです。ラムダの簡潔さは型推論と省略に起因しますが、returnこれはこの質問が求めるものに関連するとは思いません。
svick 14

2

理由は、これらの言語が十分に機能していないためかもしれません。つまり、関数を定義することはめったにありません。したがって、余分なキーワードを使用してもかまいません。

MLまたはミランダの伝統、OTOHの言語では、ほとんどの場合関数を定義します。たとえば、いくつかのHaskellコードを見てください。文字通りほとんどが関数定義のシーケンスであり、それらの多くはローカル関数とそれらのローカル関数のローカル関数を持っています。したがって、Haskellの楽しいキーワードは、assignで始まる命令型言語での声明文を要求するのと同じくらい大きな間違いです。原因の割り当ては、おそらく最も頻度の高い単一のステートメントです。


1

個人的には、あなたのアイデアに致命的な欠陥はないと思います。新しい構文を使用して特定のことを表現するのが予想よりもトリッキーであるか、および/または(さまざまな特殊なケースやその他の機能を追加するなど)修正する必要があると感じるかもしれませんが、アイデアを完全に放棄する必要があります。

あなたが提案した構文は、数学で関数や関数のタイプを表現するために時々使用されるいくつかの表記スタイルのバリアントに似ています。これは、すべての文法と同様に、おそらく一部のプログラマーにとって他のプログラマーよりも魅力的であることを意味します。(数学者として、たまたま好きです。)

ただし、ほとんどの言語では、def-style構文(つまり、従来の構文)標準の変数割り当てとは異なる動作をすることに注意してください。

  • ではCC++家族、機能は一般的に、すなわち、「オブジェクト」、入力されたデータのチャンクをコピーして、スタックやその他もろもろの上に置くべきものとして扱われていません。(はい、関数ポインターを使用できますが、それらは通常の意味で「データ」ではなく実行可能なコードを指します。)
  • ほとんどのオブジェクト指向言語では、メソッド(つまり、メンバー関数)に特別な処理があります。つまり、クラス定義のスコープ内で宣言された関数だけではありません。最も重要な違いは、メソッドが呼び出されるオブジェクトは、通常、暗黙的な最初のパラメーターとしてメソッドに渡されることです。Pythonはこれを明示的に使用しますself(ところで、これは実際にはキーワードではありません。任意の有効な識別子をメソッドの最初の引数にすることができます)。

新しい構文がコンパイラーまたはインタープリターが実際に実行していることを正確に(そしてできれば直感的に)表すかどうかを考慮する必要があります。例えば、Rubyのラムダとメソッドの違いを理解するのに役立つかもしれません。これにより、関数が単なるデータであるというパラダイムが、典型的なオブジェクト指向/手続き型のパラダイムとどのように異なるかがわかります。


1

一部の言語では、関数は値ではありません。と言うような言語で

val f = << i : int  ... >> ;

は関数定義ですが、

val a = 1 ;

1つの構文を使用して2つのことを意味するため、定数を宣言し、混乱を招きます。

ML、Haskell、Schemeなどの他の言語は、関数を第1クラス値として扱いますが、関数値定数を宣言するための特別な構文をユーザーに提供します。つまり、構造が一般的で冗長な場合は、ユーザーに短縮形を与える必要があります。まったく同じことを意味する2つの異なる構文をユーザーに提供することは不適切です。時には優雅さが実用性に犠牲にされるべきです。

もしあなたの言語で、関数が第一級であるなら、構文糖を見つけようとしないほど簡潔な構文を見つけてみませんか?

-編集-

(まだ)誰も取り上げていない別の問題は、再帰です。許可する場合

{ 
    val f = << i : int ... g(i-1) ... >> ;
    val g = << j : int ... f(i-1) ... >> ;
    f(x)
}

そしてあなたが許可する

{
    val a = 42 ;
    val b = a + 1 ;
    a
} ,

それはあなたが許可するべきであるということですか

{
    val a = b + 1 ; 
    val b = a - 1 ;
    a
} ?

怠zyな言語(Haskellなど)では、ここで問題は発生しません。静的チェックが本質的にない言語(LISPなど)では、ここで問題は発生しません。しかし、静的にチェックされた熱心な言語では、最初の2つを許可し、最後の2つを禁止したい場合、静的チェックのルールがどのように定義されるかに注意する必要があります。

-編集の終了-

* Haskellはこのリストに属さないと主張されるかもしれません。関数を宣言する方法は2つありますが、ある意味では、どちらも他の型の定数を宣言するための構文を一般化したものです。


0

これは、型がそれほど重要ではない動的言語では便利かもしれませんが、変数の型を常に知りたい静的型言語では読みにくいです。また、オブジェクト指向言語では、変数がサポートする操作を知るために、変数の型を知ることが非常に重要です。

あなたの場合、変数が4つの関数は次のようになります。

(int, long, double, String => int) addOne = (x, y, z, s) => {
  return x + 1;
}

関数ヘッダーを見て(x、y、z、s)を見ると、これらの変数のタイプがわかりません。z3番目のパラメーターである型を知りたい場合は、関数の先頭を調べて、1、2、3のカウントを開始し、型がであることを確認する必要がありdoubleます。前者の方法で私は直接見て見るdouble z


3
もちろん、型推論と呼ばれるすばらしいものがあります。これによりvar addOne = (int x, long y, double z, String s) => { x + 1 }、選択した非モロニックな静的型付け言語(例:C#、C ++、Scala)のようなものを書くことができます。C#で使用される他の点では非常に限られたローカル型推論でさえ、これには完全に十分です。したがって、この答えは、そもそも疑わしい特定の構文を批判しているだけで、実際にはどこでも使用されていません(Haskellの構文にも非常に似た問題があります)。
アモン14

4
@amon彼の答えは構文を批判しているかもしれませんが、質問の目的の構文は誰にとっても「自然」ではないかもしれないことも指摘しています。

1
@amon型推論の有用性は、誰にとっても素晴らしいことではありません。コードを書くよりも頻繁にコードを読む場合、コードを読むときに頭の中で型を推測しなければならないため、型推論は悪いです。どのタイプが使用されているかを実際に考えるよりも、タイプを読み取る方がはるかに高速です。
m3th0dman 14

1
批判は私にとって妥当なようです。Javaの場合、OPは[入力、出力、名前、入力]は単純に[出力、名前、入力]よりも単純で明確な関数defであると考えているようです。@ m3th0dmanが述べているように、署名が長くなると、OPの「よりクリーンな」アプローチは非常に長くなります。
マイケル14

0

ほとんどの言語でこのような区別をする非常に単純な理由があります。評価宣言を区別する必要があります。あなたの例は良いです:なぜ変数が好きではないのですか?さて、変数式はすぐに評価されます。

Haskellには、評価と宣言を区別しない特別なモデルがあります。そのため、特別なキーワードは必要ありません。


2
しかし、ラムダ関数の一部の構文もこれを解決するので、なぜそれを使用しないのでしょうか?
svick 14

0

関数は、ほとんどの言語でリテラルやオブジェクトなどとは異なる方法で宣言されます。関数の使用方法、デバッグ方法、エラーの原因が異なる可能性があるためです。

動的オブジェクト参照または可変オブジェクトが関数に渡されると、関数は実行中にオブジェクトの値を変更できます。この種の副作用により、関数が複雑な式の中にネストされている場合、関数が何をするかを追跡することが難しくなる可能性があります。これは、C ++やJavaなどの言語でよくある問題です。

すべてのオブジェクトにtoString()操作があるJavaのある種のカーネルモジュールのデバッグを検討してください。toString()メソッドはオブジェクトを復元することが期待される場合がありますが、その値をStringオブジェクトに変換するには、オブジェクトを逆アセンブルおよび再アセンブルする必要がある場合があります。toString()が(フックとテンプレートのシナリオで)呼び出して作業を行い、ほとんどのIDEの変数ウィンドウでオブジェクトを誤って強調表示するメソッドをデバッグしようとすると、デバッガーがクラッシュする可能性があります。これは、IDEがデバッグ中のコードを呼び出すオブジェクトをtoString()しようとするためです。プリミティブ値の意味的な意味は、プログラマーではなく言語によって定義されるため、プリミティブ値がこのようにくだらないことはありません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.