Squeak Smalltalk にあって Ruby にない文字列操作


大山鳴動して鼠(演算処理。後述)一匹…って感じ?


文字列クラスのメソッド数比較 に絡めた Ruby にあって Squeak Smalltalk にはない文字列操作 の続き。で、まずふと疑問に思ったのですが、

(String.instance_methods - Object.instance_methods).size

というのは、

(String.instance_methods - String.superclass.instance_methods).size

という解釈をすべきだったんでしょうかね。それならば、Squeak Smalltalk の場合 String は Object の直下にはないので、

(String allSelectors difference: String superclass allSelectors) size  "=> 195 "

とだいぶ目減りします。


でも、Ruby の String はモジュールの Comparable や Enumerable も継承していて、これらは相変わらず勘定に入っているみたいなので、やはり、Object と ProtoObject の分を除くだけでよさそうだということにしました。数も 500 とキリが良いですし。w

((String allSuperclasses difference: {ProtoObject. Object})
    inject: (String allSelectors difference: Object allSelectors)
    into: [:set :class | set addAll: class selectors; yourself]) size   "=> 500 "


Ruby では同じことをするとこうなります。手元の環境では Ruby 1.8 が 108、Ruby 1.9 が 105 でした。

((String.instance_methods - Object.instance_methods) | String.instance_methods(false)).size


で。結論としては、正直なところ 500 vs 100 という数字から期待されるほどの違いは無かったです。爆

というのも、Squeak Smalltalk には、Smalltalk の文法上(引数の数を変えられない)や仕様上(引数の順番を変えられない、オーバーロードできない、正規表現をデフォルトでサポートしない)の制約から同じようなメソッドをいくつも定義しなければならない実情があり、単純にメソッド数だけはカサ高くなるという傾向があるからです。



まず、整理のため 500 個のメソッドをざっと眺めて、自分なりにいくつかにグループ分けをしてみました。全メソッドとその分類は最後に示します。すでに述べたように、両者で共通のメソッドは(Ruby 1.8 で意味をもたない Enemerable なものを除けば)約 30 個で、残りの 470 個の内訳は、

  • 演算 6 個
  • アクセス、部分抽出、追加、再構成 129 個
  • 比較、判断 40 個
  • 文字の列挙 41 個
  • 文字や部分文字列の検索 34 個
  • 置換 3 個
  • 型変換、情報抽出 59 個
  • 多言語処理用 17 個
  • フロー制御 7 個
  • GUI 11 個
  • 実装不適、使用不可、機能するが意味をなさない 48 個
  • プライベート、内部処理、内部情報、GUI情報 65 個
  • ダックタイピング? 9 個


というようになります。こういうときこそメソッドカテゴリの活躍がっ!とも思ったのですが、もともと Squeak Smalltalk のメソッドカテゴリはさほど整理されていなかったところを、最近は Monticello がトドメを刺したくれた感じで、いざってときにまったく役に立たないものになってしまっていたため、結局手作業で…。orz ひじょーに残念です。Smalltalkアスペクト指向っぽい考え方を(処理系の前にまず)環境のほうに入れてゆくようにしないとダメかもわからんね。


フロー制御、GUIは Ruby には関係ないので無視。多言語関係も除きます。よく調べると、意味をなさないメソッドも 50 個近くありRuby 1.8 の Enumerable のことは笑えません(^_^;)。内部利用されているメソッドや、おそらくダックタイピング用に設けられた self を返すだけのメソッドも除くと、残りは 312 個になります。

前述の通り、Ruby と同条件なら必要なさそうなメソッド数をざっくり半数ほどと見積もると実質は 150 個程度? かろうじて優勢であっても“圧倒”するほどではなさそうです。正規表現のことを加味すれば、まあ、負けでしょうね。そんな中で、Ruby になさそうなものを無理矢理ひねり出して挙げてみました。


▼ 文字列を数値に見立てた演算

Squeak Smalltalk では、四則演算他(#* #+ #- #/ #// #\\)を、文字列同士、あるいは別の数値とともに行なえるようになっています。結果には、引数の型に一致した値が返ります。Ruby は文字列の連結に #+ や #* を使ってしまっているので、この切り口では Perl より Python 寄りなのでしょうね。

Ruby
"3" + "4"   #=> "34"
"3" +  4    #=> TypeError
Squeak Smalltalk
'3' + '4'   "=> '7' "
'3' +  4    "=>  7  "


なお、Squeak Smalltalk 以外の Smalltalk では、たいてい String に #+ が無いのでエラーになるので注意してください。

Cincom SmalltalkVisualWorks
'3' + '4"   "=> Unhandled exception "


この機能は、いつ誰が仕込んだものだろう…とふと興味を持ったので、Apple Smalltalk で試してみましたが、エラーになりました。ちなみに Apple SmalltalkSqueak の前身で、XEROX 純正の Smalltalk-80 のサブセットながら、カーリーブレイスによる動的配列定義や、これを用いた switch-case 式、多重代入の導入(これはのちに廃止)など、Smalltalk 的にはある意味“掟破り”な拡張も施された処理系です。

改めて Squeak のバージョンを遡って当該メソッドの情報をたどって調べてみると、1998 年ごろにダン・インガルスが追加したらしいことがわかりました。ちなみに、引数の型で振る舞いを変えるこのメソッドは、有名な“ダブルディスパッチ”を用いて実現されています。

String >> + arg
    ^ arg adaptToString: self andSend: #+
String >> adaptToString: rcvr andSend: selector
    ^ (rcvr asNumber perform: selector with: self asNumber) printString
Number >> adaptToString: rcvr andSend: selector
    ^ rcvr asNumber perform: selector with: self

▼配列としての操作

Ruby と違って Smalltalk には文字オブジェクトがあり、文字列はその文字オブジェクトを要素とする特殊な配列として位置づけられています。これは、豊富な配列操作が原則としてそのまま文字列にも使用可能であることを意味します。したがって今回の比較は、文字列操作メソッド数同士の差というよりは、文字列が配列として振舞えるか否かの差と見て、同じ手間をかけるなら“Squeak Smalltalk にあって Ruby にはない配列操作”のほうを探した方が有意義だったかもしれませんね。

なお、Ruby の Array と、Squeak Smalltalk の Array および OrderedCollection との対応は id:sumim:20050830:p1 にまとめてあるのでよかったらご覧ください。

Squeak Smalltalk
'abc123' do: [:each | Transcript cr; show: each]   "=> 各文字を出力 "
'abc123' collect: [:each | each asUppercase]       "=> 'ABC123' "
'abc123' select: [:each | each isDigit]            "=> '123' "
'abc123' inject: '' into: [:result :each | each asString, result ]   "=> '321cba' "


注意として、たとえば #collect: のようなブロックの返り値を文字列として再構成するタイプのメソッドでは、ブロックが返す要素も文字でなければいけないという制約があります。

'abc123' collect: [:each | each asciiValue]        "=> Error "


Ruby 1.8 の文字列の Enumerable メソッドは 壊れています。 文字単位ではなく行単位に動くようになっています。(コメント欄で tociyuki さんに認識違いをご指摘頂きました。ありがとうございます。次の動作例も差し替えました。)

Ruby 1.8
"abc\n123".each{ |e| p e }         # "abc\n" "123" を表示
"abc\n123".collect{ |e| e.succ }   #=> ["abd\n","125"]
"abc\n123".select{ |e| e < "A" }   #=> ["123"]
"abc\n123".inject(""){ |r,e| e + r}   #=> "123abc\n"


Ruby 1.9 では #each_byte、#each_char、#each_line を使うことで単位(要素)を明示的にして、#each の代わりを、そのほかの Enumerable な操作については、ブロックを与えずにこれらのメソッドを起動した際に返される Enumerator というプロキシを介して行なうことが可能になったようです。Ruby の Enumerator は Smalltalk のストリームにちょっと似ていますね。

Ruby 1.9
"abc123".each{ |e| p e }         #=> NoMethodError
"abc123".collect{ |e| e.succ }   #=> NoMethodError
"abc123".select{ |e| e < "A" }   #=> NoMethodError
"abc123".inject(""){ |r,e| e + r}   #=> NoMethodError
"abc123".each_char{ |e| p e }    # 各文字を出力
"abc123".each_char.class         #=> Enumerable::Enumerator
"abc123".each_char.collect{ |e| e.succ }   #=> ["b", "c", "d", "2", "3", "4"]
"abc123".each_char.select{ |e| e < "A" }   #=> ["1", "2", "3"]
"abc123".each_char.inject(""){ |r,e| e + r}   #=> "321cba"


なお、Squeak Smalltalk では、文字以外のバイトや行単位でのアクセスや列挙操作は、専用のメソッド(#byteAt:、#byteAt:put:、#lineNumber:、 #linesDo:)を使うか、配列に変換(#asByteArray、#subStrings:)して行ないます。


▼冗長(redundant)なメソッド

簡潔さを好む Ruby にないのは当たり前ですが。いちおう(^_^;)。

そうはいっても、Ruby に対しては、ファウラー言うところの“ヒューメイン”、ベック言うところの“インテンション・リビーリング”なメソッドが Smalltalk に比べて貧弱…という印象をもっているので、まったく的はずれということでもないと考えます。

ただ、近頃は Ruby にも 1.8 で Squeak Smalltalk の #first、#first:、#last、#last: に当たる #first や #last(残念ながらこれらは文字列には直接は使えませんけれど…)が、1.9 では同じく #beginsWith:、#endsWith: に当たる #start_with? や #end_with? が追加され、この分野でも地味に強化されつつあるようです。

Squeak Smalltalk では正規表現をデフォルトではサポートしないという制約から #subStrings やトークンを扱う検索メソッド群が多数存在するわけですが、逆に考えてこれら冗長なメソッドは、正規表現を用いて同じことをする #scan や #gsub よりも読みやすいという負け惜しみも。



以下に全メソッドと今回行なったグループ分けを示します。


"◆両者で共通( Ruby のメソッドとの対応は id:sumim:20080324:p1 を参照)"

#(#size #at: #at:put: #, #isEmpty #reverse #reversed #asUppercase #translateToUppercase #asLowercase #translateToLowercase #capitalized #asInteger #asString #asSymbol #do: #linesDo: #includes: #become: #copyWithout: #copyFrom:to: #withBlanksTrimmed #withoutTrailingBlanks #withoutLeadingDigits #allButLast #padded:to:with: #< #> #>= #<= #compare:caseSensitive: #compare: #hash #= ) size "=> 34 " - 4


"◆それ以外"

+"演算"

#(#* #+ #- #/ #'//' #'\\' ) size "=> 6 "


+"アクセス、部分抽出、追加、再構成"

#(#after: #after:ifAbsent: #allButFirst #allButFirst: #allButLast: #anyOne #at:ifAbsent: #at:incrementBy: #atAll: #atAll:put: #atAll:putAll: #atAllPut: #atLast: #atLast:ifAbsent: #atLast:put: #atPin: #atRandom #atRandom: #atWrap: #atWrap:put: #before: #before:ifAbsent: #byteAt: #byteAt:put: #byteSize #checkedAt: #detect: #detect:ifNone: #detectMax: #detectMin: #detectSum: #first #first: #second #third #fourth #fifth #sixth #seventh #eighth #ninth #last #last: #lineCorrespondingToIndex: #lineCount #lineNumber: #max #median #middle #min #nextToLast #occurrencesOf: #copyAfter: #copyAfterLast: #copyEmpty #copyLast: #copyReplaceAll:with: #copyReplaceAll:with:asTokens: #copyReplaceFrom:to:with: #copyReplaceTokens:with: #copyUpTo: #copyUpToLast: #copyWith: #copyWithFirst: #copyWithoutAll: #copyWithoutFirst #copyWithoutIndex: #withCRs unparenthetically#asAlphaNumeric:extraChars:mergeUID: #asCommaString #asCommaStringAnd #asPostscript #asSmalltalkComment #asHex #asHtml #asIRCLowercase #asIdentifier: #asPluralBasedOn: #asFileName #asSqueakPathName #asUnHtml #compressWithTable: #contractTo: #convertFromSuperSwikiServerString #convertToSystemString #convertToSuperSwikiServerString #decodeMimeHeader #decodeQuotedPrintable #encodeDoublingQuoteOn: #encodeForHTTP #escapeEntities #expandMacros #expandMacrosWith: #expandMacrosWith:with: #expandMacrosWith:with:with: #expandMacrosWith:with:with:with: #expandMacrosWithArguments: #flipRotated: #forceTo:paddingStartWith: #forceTo:paddingWith: #format: #macToSqueak #sansPeriodSuffix #squeakToIso #squeakToMac #surroundedBySingleQuotes #truncateTo: #truncateWithElipsisTo: #unescapePercents #unzipped #withFirstCharacterDownshifted #withBlanksCondensed #withInternetLineEndings #withNoLineLongerThan: #withSeparatorsCompacted #withSqueakLineEndings #withoutLeadingBlanks #withoutQuoting #withoutTrailingDigits #isoToSqueak #shuffled #shuffledBy: #sort #sort: #sortBy: #topologicallySortedUsing: #upTo: #shallowCopy ) size "=> 129 "


+"比較、判断"

#(#allSatisfy: #anySatisfy: #beginsWith: #contains: #endsWith: #endsWithAColon #endsWithAnyOf: #endsWithDigit #identityIncludes: #includesAllOf: #includesAnyOf: #includesSubString: #includesSubstring:caseSensitive: #includesSubstringAnywhere: #isAllDigits #isAllSeparators #isAsciiString #isByteString #isEmptyOrNil #isOctetString #isSequenceable #isSorted #isSortedBy: #isWideString #noneSatisfy: #notEmpty #onlyLetters #sameAs: #startsWith: #startsWithDigit #alike: #caseInsensitiveLessOrEqual: #caseSensitiveLessOrEqual: #charactersExactlyMatching: #match: #howManyMatch: #difference: #union: #intersection: #isCollection ) size "=> 40 "


+"文字の列挙"

#(#allButFirstDo: #allButLastDo: #asDigitsToPower:do: #associationsDo: #collect: #collect:from:to: #collect:thenDo: #collect:thenSelect: #collectWithIndex: #combinations:atATimeDo: #do:separatedBy: #do:toFieldNumber: #do:without: #doWithIndex: #from:to:do: #from:to:put: #groupBy:having: #groupsOf:atATimeCollect: #groupsOf:atATimeDo: #inject:into: #keysAndValuesDo: #count: #overlappingPairsCollect: #overlappingPairsDo: #overlappingPairsWithIndexDo: #paddedWith:do: #pairsCollect: #pairsDo: #permutationsDo: #reject: #reject:thenDo: #reverseDo: #reverseWith:do: #select: #select:thenCollect: #select:thenDo: #tabDelimitedFieldsDo: #with:collect: #with:do: #withIndexCollect: #withIndexDo: ) size "=> 41 "


+"文字や部分文字列の検索"

#(#findAnySubStr:startingAt: #findBetweenSubStrs: #findCloseParenthesisFor: #findDelimiters:startingAt: #findFirst: #findLast: #findLastOccuranceOfString:startingAt: #findSelector #findString: #findString:startingAt: #findString:startingAt:caseSensitive: #findSubstring:in:startingAt:matchTable: #findTokens: #findTokens:escapedBy: #findTokens:includes: #findTokens:keep: #findWordStart:startingAt: #identityIndexOf: #identityIndexOf:ifAbsent: #indexOf: #indexOf:ifAbsent: #indexOf:startingAt: #indexOf:startingAt:ifAbsent: #indexOfAnyOf: #indexOfAnyOf:ifAbsent: #indexOfAnyOf:startingAt: #indexOfAnyOf:startingAt:ifAbsent: #indexOfSubCollection: #indexOfSubCollection:startingAt: #indexOfSubCollection:startingAt:ifAbsent: #lastIndexOf: #lastIndexOf:ifAbsent: #lastIndexOf:startingAt:ifAbsent: #lastSpacePosition ) size "=> 34 "


+"フロー制御"

#(#ifEmpty: #ifEmpty:ifNotEmpty: #ifEmpty:ifNotEmptyDo: #ifNotEmpty: #ifNotEmpty:ifEmpty: #ifNotEmptyDo: #ifNotEmptyDo:ifEmpty: ) size "=> 7 "


+"置換"

#(#replaceFrom:to:with: #replaceFrom:to:with:startingAt: #swap:with: ) size "=> 3 "


+"型変換、情報抽出"

#(#asArray #asBag #asByteArray #asByteString #asCharacter #asCharacterSet #asColorArray #asCubic #asDate #asDateAndTime #asDuration #asFloatArray #asFourCode #asIdentitySet #asIdentitySkipList #asIntegerArray #asNumber #asOctetString #asPacked #asParagraph #asPointArray #asSet #asSignedInteger #asSkipList #asSkipList: #asSortedArray #asSortedCollection #asSortedCollection: #asText #asTime #asTimeStamp #asURI #asUnsignedInteger #asUrl #asUrlRelativeTo: #asVersion #asVmPathName #asWideString #asWordArray #extractNumber #initialIntegerOrNil #crc16 #keywords #numArgs #numericSuffix #readStream #romanNumber #service #serviceOrNil #splitInteger #stemAndNumericSuffix #asStringWithCr #correctAgainst: #writeStream #indentationIfBlank: #subStrings #subStrings: #substrings #asOrderedCollection ) size "=> 59 "


+"多言語処理用"

#(#convertFromEncoding: #convertFromWithConverter: #convertToEncoding: #convertToWithConverter: #encodeForHTTPWithTextEncoding: #encodeForHTTPWithTextEncoding:conditionBlock: #includesUnifiedCharacter #isoToUtf8 #leadingCharRunLengthAt: #translateFrom:to:table: #translateWith: #translated #translatedIfCorresponds #translatedTo: #unescapePercentsWithTextEncoding: #utf8ToIso #writeLeadingCharRunsOn: ) size "=> 17 "


+"GUI"

#(#asDisplayText #asKnownNameMenu #chooseOne: #displayAt: #displayOn: #displayOn:at: #displayOn:at:textColor: #displayProgressAt:from:to:during: #do:displayingProgress: #explorerContents #openInWorkspaceWithTitle: ) size "=> 11 "


+"実装不適、使用不可、機能するが意味をなさない"

#(#@ #abs #arcCos #arcSin #arcTan #average #ceiling #cos #degreeCos #degreeSin #add: #add:withOccurrences: #addAll: #addIfNotPresent: #asTraitComposition #concatenation #exp #floor #gather: #literalIndexOf:ifAbsent: #ln #log #negated #polynomialEval: #raisedTo: #range #reciprocal #remove: #remove:ifAbsent: #removeAll: #removeAllFoundIn: #removeAllSuchThat: #replaceAll:with: #roundTo: #rounded #sign #sin #sqrt #squared #sum #swapHalves #tan #truncated #write: #findBinary: #findBinary:ifNone: #findBinaryIndex: #findBinaryIndex:ifNone: ) size "=> 48 "


+"プライベート、内部処理、内部情報、GUI情報"

#(#adaptToCollection:andSend: #adaptToComplex:andSend: #adaptToNumber:andSend: #adaptToPoint:andSend: #adaptToString:andSend: #asDigitsAt:in:do: #capacity #changeInSlopes: #changeOfChangesInSlopes: #closedCubicSlopes #closedCubicSlopes: #asStringOn:delimiter: #asStringOn:delimiter:last: #combinationsAt:in:after:do: #compare:with:collated: #correctAgainst:continuedFrom: #correctAgainstDictionary:continuedFrom: #correctAgainstEnumerator:continuedFrom: #cubicPointPolynomialAt: #defaultElement #errorEmptyCollection #errorFirstObject: #errorLastObject: #errorNoMatch #errorNotFound: #errorNotKeyed #errorOutOfBounds #evaluateExpression:parameters: #getEnclosedExpressionFrom: #getInteger32: #hasEqualElements: #assertSlopesWith:from:to: lastIndexOfPKSignature: #mergeFirst:middle:last:into:by: #mergeSortFrom:to:by: #mergeSortFrom:to:src:dst:by: #naturalCubicSlopes #naturalCubicSlopes: #naturalFillinList #closedFillinList #nilTransitions #permutationsStartingAt:do: #printElementsOn: #printNameOn: #printOn:delimiter: #printOn:delimiter:last: #putInteger32:at: #restoreEndianness #segmentedSlopes #slopesWith:from:to: #startingAt:match:startingAt: #storeElementsFrom:to:on: #toBraceStack: #transitions #transitions: #writeOn: #writeOnGZIPByteStream: #bytesPerBasicElement #bytesPerElement #emptyCheck #skipAnySubStr:startingAt: #skipDelimiters:startingAt: #asLegalSelector #storeOn: #flattenOnStream: #printOn: ) size "=> 66 "


+"ダックタイピング?"

#(#asDefaultDecodedString #copyWithDependent: #stringhash #contents #integerAt: #integerAt:put: #askIfAddStyle:req: #string #isZero) size "=> 9 "


"合計 => 500"