XSLT3.0への道(34) "=>" と" !" と高階関数でチャチャッとやってしまう.

普段Javaなんてめったに使わないのですが、たまたまコーディングをせざるを得なくなり、同業の女房の持っている本を見たら「Javaによる関数プログラミング」というのがあったので読んでみました.
Java8になって追加されたメソッドを使い、コレクションをスマートに処理する例が載っています.例えば

final List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scotto");

とあったとき、これを1行毎に列挙して出力するのを昔はfor文で

for(int i=0; i < friends.size(); i++){
  System.out.println(friends.get(i));
}

なんてベタでやっていたのを、IterableインタフェースにあるforEach()というメソッドを使って、

friends.forEach((name) -> System.out.println(name));

とするのが今のやり方.

また、大文字に変換して1行に列挙するのはStreamインタフェースに追加されたmap()メソッドで

friends.stream()
           .map(name -> name.toUppercase())
           .forEach(name -> System.out.print(name + " "));
System.out.println();

とやってしまいます.昔のfor文で回すイメージとは随分変わったものです.

ではXSLTではどうなのか?ちょっと試してみました.

まず次のような準備を整えます.ahf:system.out.printlnという関数でメッセージを出力します.XSLTではXPath式からは間接に関数を介さないと標準出力に出せないからです.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:math="http://www.w3.org/2005/xpath-functions/math"
    xmlns:ahf="http://www.antennahouse.com/names/XSLT/Functions/Document"
    exclude-result-prefixes="#all"
    version="3.0">
    
    <xsl:variable name="friends" as="xs:string+" select="('Brian','Nate','Neal','Raju','Sara','Scott')"/>
    
    <xsl:function name="ahf:system.out.println" as="empty-sequence()">
        <xsl:param name="prmStr" as="xs:string"/>
        <xsl:message select="$prmStr"/>
    </xsl:function>

    <xsl:template match="/">
        <!-- ここにfriendに対してやりたい処理を書く-->
    </xsl:template>

</xsl:stylesheet>

名前を1行毎に列挙して出力する.

これはSimple Map Operator !を使えばいとも簡単です.決してxsl:for-eachなんて使ってはいけません.

<xsl:sequence select="$friends ! ahf:system.out.println(.)"/>
[結果:oXygenのログ]
[xslt] Brian
[xslt] Nate
[xslt] Neal
[xslt] Raju
[xslt] Sara
[xslt] Scott

名前を大文字に変換して1行に列挙する.

これも"!"とArrow Operator "=>" にupper-case()を使えば簡単です.

<xsl:sequence select="$friends ! upper-case(.) => string-join(' ') => ahf:system.out.println()"/>
[結果]
[xslt] BRIAN NATE NEAL RAJU SARA SCOTT

名前の文字数をカウントして1行に出力する.

Javaでは

friends.stream()
           .map(name -> name.length())
           .forEach(name -> System.out.print(name + ""));

XSLTでは

<xsl:sequence select="$friends ! string-length(.) => string-join(' ') => ahf:system.out.println()"/>

[結果]

[xslt] 5  4  4  4  4  5 

"N"から始まる名前の数をカウントして出力する.

Javaでは、

final List<string> startsWithN = friends
        .stream()
        .filter(name -> name.startsWith("N"))
        .collect(Collections.toList());
System.out.println(String.format("Found %d names", startsWithN.size()));

XSLTではもう少し頑張って、filter()なんてifで自前で書きます.

<xsl:variable name="times" select="$friends ! (if (starts-with(.,'N')) then . else ()) 
                                => count() 
                                => string()"/>
<xsl:sequence select="('Found '|| $times ||' times') => ahf:system.out.println()"/>
[結果]
 [xslt] Found 2 times

最も長い文字数の名前を出力する.複数あったら最初のものを出す.

Javaでは、

final Option<String> aLongName =
        friends.stream()
                   .reduce((name1, name2) ->
                      name1.length() >= name2.length()? name1 : name2);
aLongname.ifPresent(name -> System.out.println(String.format("A longest name: %s", name)));

XSLTではreduceなんていうのはないので、高階関数を使えば次のように書けます.(もっとスマートな方法あったら教えてください)

<xsl:variable name="aLongName" as="xs:string?"
        select="$friends 
               => (let $getLength := function ($strings as xs:string*){for-each($strings,function ($a as xs:string) {string-length($a)})},
                           $getMaxLength := function ($strings as xs:string*, $getLength as function (xs:string*) as xs:integer*){max($getLength($strings))},
                           $maxLength := $getMaxLength(?, $getLength),
                           $maxString := function ($strings as xs:string*){$strings[. => string-length() eq $maxLength($strings)]}
                      return $maxString(?))()    
               => head()"/>
<xsl:sequence select="if ($aLongName => exists()) then ('A longest name:' || $aLongName) => ahf:system.out.println() else ()"/>
[結果]
 [xslt] A longest name:Brian

XSLT3.0では、Simple Map Operator "!"、Arrow Operator "=>"、それに高階関数を使うとXSLT2.0では考えも及ばなかったようなコーディングができるようになります.本当に知っていて損はありません.

※ もっぷさんからコメントのありましたようにXPath 3.1ではfold-left()という関数があって、以下のように一発で最も長い文字数の名前を出すことができます.

<xsl:variable as="xs:string?" name="aLongName" select=" $friends => fold-left( '', function($name1, $name2) { if (string-length($name1) ge string-length($name2)) then $name1 else $name2 } )" />
<xsl:sequence select="if ($aLongName => exists()) then ('A longest name: ' || $aLongName) => ahf:system.out.println() else ()"/>

fold-left()関数は以下に仕様があります.

https://www.w3.org/TR/xpath-functions-31/#func-fold-left