diff --git a/M2/Macaulay2/d/actors2.dd b/M2/Macaulay2/d/actors2.dd index 1f8b7de3d3..d9b19fc98c 100644 --- a/M2/Macaulay2/d/actors2.dd +++ b/M2/Macaulay2/d/actors2.dd @@ -104,7 +104,7 @@ values(e:Expr):Expr := ( else WrongArg("a hash table or dictionary")); setupfun("values",values); -pairs(e:Expr):Expr := ( +export pairs(e:Expr):Expr := ( when e is oc:DictionaryClosure do ( -- # typical value: pairs, Dictionary, List o := oc.dictionary.symboltable; @@ -142,8 +142,13 @@ pairs(e:Expr):Expr := ( while i < length(o) do ( provide Expr(Sequence(toExpr(i),o.i)); i = i+1;))) - is o:List do pairs(Expr(o.v)) -- # typical value: pairs, BasicList, List - else WrongArg("a hash table, a sequence, a list, or a raw polynomial")); + -- # typical value: pairs, BasicList, List + is o:List do ( + r := pairs(Expr(o.v)); + when r + is s:Sequence do list(o.Class, s, o.Mutable) + else buildErrorPacket("internal error; expected a sequence")) + else applyEE(getGlobalVariable(pairsIteratorS), e)); -- # typical value: pairs, Thing, Iterator setupfun("pairs",pairs); -- operators diff --git a/M2/Macaulay2/d/actors3.d b/M2/Macaulay2/d/actors3.d index 65902ee3d0..227055e455 100644 --- a/M2/Macaulay2/d/actors3.d +++ b/M2/Macaulay2/d/actors3.d @@ -2,6 +2,7 @@ use evaluate; use actors; +use actors2; use ballarith; isOption(e:Expr):bool := ( @@ -82,7 +83,7 @@ override(e:Expr):Expr := ( if length(args) == 2 then ( when args.0 is h:HashTable do ( - if h.Mutable then WrongArg("an immutable hash table") + if h.Mutable then WrongArgImmutableHashTable() else when args.1 is v:Sequence do override(h,v,numOptions(v)) else override(h,Sequence(args.1),if isOption(args.1) then 1 else 0) ) @@ -1895,6 +1896,22 @@ map(e:Expr):Expr := ( else WrongNumArgs(2,3)); setupfun("apply",map); +applyPairs(e:Expr):Expr := ( + when e + is a:Sequence do ( + if length(a) == 2 then ( + when a.0 + is o:HashTable do ( + if o.Mutable then WrongArgImmutableHashTable(1) + else mappairs(a.1, o)) + -- # typical value: applyPairs, BasicList, Function, List + -- # typical value: applyPairs, Dictionary, Function, List + -- # typical value: applyPairs, Thing, Function, Iterator + else map(pairs(a.0), a.1)) + else WrongNumArgs(2)) + else WrongNumArgs(2)); +setupfun("applyPairs", applyPairs); + -- # typical value: scan, ZZ, Function, Thing scan(n:int,f:Expr):Expr := ( if n <= 0 then return nullE; @@ -2346,6 +2363,20 @@ scan(e:Expr):Expr := ( else WrongNumArgs(2)); setupfun("scan",scan); +-- # typical value: scanPairs, Thing, Function, Nothing +scanPairs(e:Expr):Expr := ( + when e + is a:Sequence do ( + if length(a) == 2 then ( + when a.0 + is o:HashTable do ( + if o.Mutable then WrongArgImmutableHashTable(1) + else scanpairs(a.1, o)) + else scan(pairs(a.0), a.1)) + else WrongNumArgs(2)) + else WrongNumArgs(2)); +setupfun("scanPairs", scanPairs); + nextPrime(e:Expr):Expr := ( when e is x:ZZcell do toExpr(nextPrime(x.v - oneZZ)) diff --git a/M2/Macaulay2/d/actors4.d b/M2/Macaulay2/d/actors4.d index 9185872f81..0928926e9e 100644 --- a/M2/Macaulay2/d/actors4.d +++ b/M2/Macaulay2/d/actors4.d @@ -200,6 +200,8 @@ select(e:Expr):Expr := ( else WrongNumArgs(2,5)); setupfun("select", select).Protected = false; -- will be overloaded in m2/lists.m2 and m2/regex.m2 +-- # typical value: selectPairs, HashTable, Function, HashTable +-- # typical value: selectPairs, ZZ, HashTable, Function, HashTable selectPairs(nval:int, obj:HashTable, f:Expr):Expr := ( u := newHashTable(obj.Class,obj.parent); u.beingInitialized = true; @@ -228,18 +230,18 @@ selectPairs(nval:int, obj:HashTable, f:Expr):Expr := ( "expected predicate to yield true or false")); p = p.next)); Expr(sethash(u,obj.Mutable))); +-- TODO: support iterators selectPairs(e:Expr):Expr := ( when e is a:Sequence do ( - -- # typical value: selectPairs, HashTable, Function, HashTable if length(a) == 2 then ( when a.0 is obj:HashTable do ( if obj.Mutable - then WrongArg(1, "an immutable hash table") + then WrongArgImmutableHashTable(1) else selectPairs(obj.numEntries, obj, a.1)) - else WrongArgHashTable(1)) - -- # typical value: selectPairs, ZZ, HashTable, Function, HashTable + -- # typical value: selectPairs, BasicList, Function, List + else select(pairs(a.0), a.1)) else if length(a) == 3 then ( when a.0 is n:ZZcell do ( @@ -248,9 +250,10 @@ selectPairs(e:Expr):Expr := ( when a.1 is obj:HashTable do ( if obj.Mutable - then WrongArg(2, "an immutable hash table") + then WrongArgImmutableHashTable(2) else selectPairs(toInt(n), obj, a.2)) - else WrongArgHashTable(2))) + -- # typical value: selectPairs, ZZ, BasicList, Function, List + else select(a.0, pairs(a.1), a.2, nullE, nullE))) else WrongArgZZ(1)) else WrongNumArgs(2, 3)) else WrongNumArgs(2, 3)); @@ -298,7 +301,7 @@ any(f:Expr,e:Expr):Expr := ( is b:List do Expr(any(f,b.v)) is i:ZZcell do if isInt(i) then Expr(any(f,toInt(i))) else WrongArgSmallInteger(1) is c:HashTable do - if c.Mutable then WrongArg(1,"an immutable hash table") else + if c.Mutable then WrongArgImmutableHashTable(1) else Expr(any(f,c)) else WrongArg("a list or a hash table")); any(f:Expr,a:Sequence,b:Sequence):Expr := ( diff --git a/M2/Macaulay2/d/evaluate.d b/M2/Macaulay2/d/evaluate.d index 29bb86293f..4ea4de36d9 100644 --- a/M2/Macaulay2/d/evaluate.d +++ b/M2/Macaulay2/d/evaluate.d @@ -28,6 +28,7 @@ export iteratorS := setupvar("iterator", nullE); export nextS := setupvar("next", nullE); export applyIteratorS := setupvar("applyIterator", nullE); export joinIteratorsS := setupvar("joinIterators", nullE); +export pairsIteratorS := setupvar("pairsIterator", nullE); eval(c:Code):Expr; applyEE(f:Expr,e:Expr):Expr; @@ -1709,7 +1710,7 @@ setup(LeftArrowS,assigntofun); idfun(e:Expr):Expr := e; setupfun("identity",idfun); -- # typical value: scanPairs, HashTable, Function, Nothing -scanpairs(f:Expr,obj:HashTable):Expr := ( -- obj is not Mutable +export scanpairs(f:Expr,obj:HashTable):Expr := ( -- obj is not Mutable foreach bucket in obj.table do ( p := bucket; while true do ( @@ -1719,22 +1720,10 @@ scanpairs(f:Expr,obj:HashTable):Expr := ( -- obj is not Mutable p = p.next; )); nullE); -scanpairsfun(e:Expr):Expr := ( - when e is a:Sequence do - if length(a) == 2 - then when a.0 is o:HashTable - do - if o.Mutable - then WrongArg("an immutable hash table") - else scanpairs(a.1,o) - else WrongArgHashTable(1) - else WrongNumArgs(2) - else WrongNumArgs(2)); -setupfun("scanPairs",scanpairsfun); mpre():Expr := buildErrorPacket("applyPairs: expected function to return null, a sequence of length 2, or an option x=>y"); -- # typical value: applyPairs, HashTable, Function, HashTable -mappairs(f:Expr,o:HashTable):Expr := ( -- o is not Mutable +export mappairs(f:Expr,o:HashTable):Expr := ( -- o is not Mutable x := newHashTable(o.Class,o.parent); x.beingInitialized = true; foreach bucket in o.table do ( @@ -1762,18 +1751,6 @@ mappairs(f:Expr,o:HashTable):Expr := ( -- o is not Mutable p = p.next; )); Expr(sethash(x,o.Mutable))); -mappairsfun(e:Expr):Expr := ( - when e is a:Sequence do - if length(a) == 2 - then when a.0 is o:HashTable - do - if o.Mutable - then WrongArg("an immutable hash table") - else mappairs(a.1,o) - else WrongArgHashTable(1) - else WrongNumArgs(2) - else WrongNumArgs(2)); -setupfun("applyPairs",mappairsfun); -- # typical value: applyKeys, HashTable, Function, HashTable export mapkeys(f:Expr,o:HashTable):Expr := ( -- o is not Mutable @@ -1824,7 +1801,7 @@ mapkeysfun(e:Expr):Expr := ( then when a.0 is o:HashTable do if o.Mutable - then WrongArg("an immutable hash table") + then WrongArgImmutableHashTable() else if length(a) == 2 then mapkeys(a.1,o) else mapkeysmerge(a.1,o,a.2) else WrongArgHashTable(1) else WrongNumArgs(2,3) @@ -1864,7 +1841,7 @@ mapvaluesfun(e:Expr):Expr := ( then when a.0 is o:HashTable do if o.Mutable - then WrongArg("an immutable hash table") + then WrongArgImmutableHashTable() else mapvalues(a.1,o) else WrongArgHashTable(1) else WrongNumArgs(2) @@ -2066,18 +2043,18 @@ combine(e:Expr):Expr := ( is v:Sequence do if length(v) == 5 then ( when v.0 is x:HashTable do - if x.Mutable then WrongArg(1,"an immutable hash table") else + if x.Mutable then WrongArgImmutableHashTable(1) else when v.1 is y:HashTable do - if y.Mutable then WrongArg(2,"an immutable hash table") else + if y.Mutable then WrongArgImmutableHashTable(2) else combine(v.2,v.3,v.4,x,y) else WrongArgHashTable(2) else WrongArgHashTable(1) ) else if length(v) == 6 then ( when v.0 is x:HashTable do - if x.Mutable then WrongArg(1,"an immutable hash table") else + if x.Mutable then WrongArgImmutableHashTable(1) else when v.1 is y:HashTable do - if y.Mutable then WrongArg(2,"an immutable hash table") else + if y.Mutable then WrongArgImmutableHashTable(2) else twistCombine(v.2,v.3,v.4,v.5,x,y) else WrongArgHashTable(2) else WrongArgHashTable(1) diff --git a/M2/Macaulay2/d/expr.d b/M2/Macaulay2/d/expr.d index ab258c77ac..7958fd5a31 100644 --- a/M2/Macaulay2/d/expr.d +++ b/M2/Macaulay2/d/expr.d @@ -389,6 +389,8 @@ export WrongArgMatrix(n:int):Expr := WrongArg(n,"a raw matrix"); export WrongArgMatrix():Expr := WrongArg("a raw matrix"); export WrongArgHashTable():Expr := WrongArg("a hash table"); export WrongArgHashTable(n:int):Expr := WrongArg(n, "a hash table"); +export WrongArgImmutableHashTable():Expr := WrongArg("an immutable hash table"); +export WrongArgImmutableHashTable(n:int):Expr := WrongArg(n, "an immutable hash table"); export ArgChanged(name:string,n:int):Expr := ( buildErrorPacket(quoteit(name) + " expected argument " + tostring(n) + " not to change its type during execution")); diff --git a/M2/Macaulay2/d/hashtables.dd b/M2/Macaulay2/d/hashtables.dd index ca3018f8be..316b9df4b9 100644 --- a/M2/Macaulay2/d/hashtables.dd +++ b/M2/Macaulay2/d/hashtables.dd @@ -255,17 +255,19 @@ export toHashTable(v:Sequence):Expr := ( Expr(sethash(o,false))); toHashTable(e:Expr):Expr := ( when e + -- # typical value: hashTable, BasicList, HashTable is w:List do toHashTable(w.v) is s:Sequence do ( - if length(s) != 2 then return WrongNumArgs(1,2); - when s.0 - is FunctionClosure do nothing - is CompiledFunction do nothing - is CompiledFunctionClosure do nothing - else return WrongArg(1,"a function"); - when s.1 is w:List do toHashTableWithCollisionHandler(w.v,s.0) - else WrongArg(2,"a list")) - else WrongArg("a list")); + if length(s) == 2 then ( + if isFunction(s.0) then ( + when s.1 + -- # typical value: hashTable, Function, BasicList, HashTable + is w:Sequence do toHashTableWithCollisionHandler(w, s.0) + is w:List do toHashTableWithCollisionHandler(w.v, s.0) + else WrongArg(2, "a list or sequence")) + else toHashTable(s)) + else toHashTable(s)) + else WrongArg("a list or sequence")); setupfun("hashTable",toHashTable); copy(table:array(KeyValuePair)):array(KeyValuePair) := ( diff --git a/M2/Macaulay2/d/pthread.d b/M2/Macaulay2/d/pthread.d index f396bba20f..acd635f8e9 100644 --- a/M2/Macaulay2/d/pthread.d +++ b/M2/Macaulay2/d/pthread.d @@ -42,14 +42,6 @@ startup(tb:TaskCellBody):null := ( compilerBarrier(); null()); -isFunction(e:Expr):bool := ( - when e - is CompiledFunction do true - is CompiledFunctionClosure do true - is FunctionClosure do true - is s:SpecialExpr do isFunction(s.e) - else false); - taskDone(tb:TaskCellBody):bool := tb.resultRetrieved || taskDone(tb.task); cancelTask(tb:TaskCellBody):Expr := ( diff --git a/M2/Macaulay2/d/util.d b/M2/Macaulay2/d/util.d index c920c88967..0403063f2b 100644 --- a/M2/Macaulay2/d/util.d +++ b/M2/Macaulay2/d/util.d @@ -187,6 +187,14 @@ export getSequenceOfMutableMatrices(e:Expr) : RawMutableMatrixArray := ( is a:RawMutableMatrixCell do RawMutableMatrixArray(a.p) else RawMutableMatrixArray()); +export isFunction(e:Expr):bool := ( + when e + is CompiledFunction do true + is CompiledFunctionClosure do true + is FunctionClosure do true + is s:SpecialExpr do isFunction(s.e) + else false); + ----------------------------------------------------------------------------- -- helper routines for checking and converting return values diff --git a/M2/Macaulay2/m2/iterators.m2 b/M2/Macaulay2/m2/iterators.m2 index ded8da8eff..fc5b6353f4 100644 --- a/M2/Macaulay2/m2/iterators.m2 +++ b/M2/Macaulay2/m2/iterators.m2 @@ -65,3 +65,11 @@ joinIterators = a -> ( r)))) Iterator | Iterator := (x, y) -> joinIterators(x, y) + +pairsIterator = x -> Iterator ( + iter := iterator x; + i := 0; + () -> ( + y := next iter; + if y === StopIteration then StopIteration + else (i, (i += 1; y)))) diff --git a/M2/Macaulay2/m2/typicalvalues.m2 b/M2/Macaulay2/m2/typicalvalues.m2 index c353e341f3..7b393266b4 100644 --- a/M2/Macaulay2/m2/typicalvalues.m2 +++ b/M2/Macaulay2/m2/typicalvalues.m2 @@ -51,8 +51,6 @@ take(BasicList,ZZ) := take(BasicList,List) := BasicList => take take(Thing,ZZ) := take(Thing,List) := List => take isMutable(Thing) := Boolean => isMutable -hashTable List := HashTable => hashTable -hashTable(Function,List) := HashTable => hashTable remove(MutableList,ZZ) := Nothing => remove remove(Database,String) := Nothing => remove remove(HashTable,Thing) := Nothing => remove diff --git a/M2/Macaulay2/packages/Macaulay2Doc/doc_tables.m2 b/M2/Macaulay2/packages/Macaulay2Doc/doc_tables.m2 index d199b436d4..80a074c480 100644 --- a/M2/Macaulay2/packages/Macaulay2Doc/doc_tables.m2 +++ b/M2/Macaulay2/packages/Macaulay2Doc/doc_tables.m2 @@ -233,16 +233,23 @@ doc /// (pairs, HashTable) (pairs, Dictionary) (pairs, BasicList) + (pairs, Thing) Headline list the pairs in a hash table, dictionary, or basic list - Usage - pairs x - Inputs - x:HashTable - or @TO Dictionary@ or @TO BasicList@ - Outputs - L:List - of all pairs {\tt (k, x#k)} + Synopsis + Usage + pairs x + Inputs + x:{HashTable, Dictionary, BasicList} + Outputs + L:List -- of all pairs @CODE "(k, x#k)"@ + Synopsis + Usage + pairs x + Inputs + x:Thing -- an instance of a class with an @TO iterator@ method installed + Outputs + :Iterator Description Text If {\tt x} is a hash table or dictionary, the pairs consist of @@ -267,6 +274,16 @@ doc /// L = {3, 5, 7}; pairs L pairs {apple, banana, carrot} + Text + If @CODE "x"@ belongs to any other class, then @TO iterator@ is called + on it, and if successful, an @TO Iterator@ object is returned. + Example + pairs "foo" + toList oo + i = pairs Iterator(() -> 5) + next i + next i + next i Caveat As the first example illustrates, pairs are not necessarily listed in any particular order. @@ -377,12 +394,14 @@ doc /// Key scanPairs (scanPairs, HashTable, Function) + (scanPairs, Thing, Function) Headline apply a function to the pairs in a hash table Usage scanPairs(t, f) Inputs - t:HashTable + t:{HashTable, BasicList, Dictionary} + or any instance of a class with an @TO iterator@ method installed f:Function Description Text @@ -393,7 +412,12 @@ doc /// Example t = hashTable {{1,8},{2,20},{3,4},{4,20}} scanPairs(t, (k,v) -> print (k+v)) - scanPairs(t, (k,v) -> if v==20 then print k) + scanPairs(t, (k,v) -> if v==20 then print k) + Text + If @CODE "t"@ is not a hash table, then @M2CODE "scan(pairs t, f)"@ is + called. + Example + scanPairs({4, 5, 6}, print) Caveat This function requires an immutable hash table. To scan the pairs in a mutable hash table, use {\tt scan(pairs t, f)}. diff --git a/M2/Macaulay2/packages/Macaulay2Doc/functions/applyPairs-doc.m2 b/M2/Macaulay2/packages/Macaulay2Doc/functions/applyPairs-doc.m2 index c49d89ee5b..1c283823b5 100644 --- a/M2/Macaulay2/packages/Macaulay2Doc/functions/applyPairs-doc.m2 +++ b/M2/Macaulay2/packages/Macaulay2Doc/functions/applyPairs-doc.m2 @@ -6,12 +6,16 @@ doc /// Key applyPairs (applyPairs,HashTable,Function) + (applyPairs,BasicList,Function) + (applyPairs,Dictionary,Function) + (applyPairs,Thing,Function) Headline apply a function to each pair in a hash table Usage applyPairs(H, f) Inputs - H:HashTable + H:{HashTable, BasicList, Dictionary} + or any instance of a class with an @TO iterator@ method installed f:Function of two arguments, returning a pair or @TO null@ Outputs @@ -22,6 +26,11 @@ doc /// H = new HashTable from {1 => 10, 2 => 15, 3 => 20} applyPairs(H, (k,v) -> (k+1, v+10)) applyPairs(H, (k,v) -> (v,k)) + Text + If @CODE "H"@ is not a hash table, then @M2CODE "apply(pairs H, f)"@ is + called. + Example + applyPairs({4, 5, 6}, (i, x) -> i * x) Caveat It is an error for the function {\tt f} to return two pairs with the same key. SeeAlso diff --git a/M2/Macaulay2/packages/Macaulay2Doc/functions/select-doc.m2 b/M2/Macaulay2/packages/Macaulay2Doc/functions/select-doc.m2 index 6f4ea9b5cc..595370671b 100644 --- a/M2/Macaulay2/packages/Macaulay2Doc/functions/select-doc.m2 +++ b/M2/Macaulay2/packages/Macaulay2Doc/functions/select-doc.m2 @@ -265,7 +265,9 @@ doc /// doc /// Key selectPairs + (selectPairs, BasicList, Function) (selectPairs, HashTable, Function) + (selectPairs, ZZ, BasicList, Function) (selectPairs, ZZ, HashTable, Function) Headline select a part of a hash table by pairs @@ -274,10 +276,10 @@ doc /// selectPairs(n, x, f) Inputs n:ZZ - x:HashTable -- must be immutable + x:{HashTable, BasicList} f:Function Outputs - :HashTable + :{HashTable, BasicList} containing all (or @VAR "n"@, if it is given) key-value pairs (@VAR "k"@,@VAR "v"@) from @VAR "x"@ for which @CODE "f(k,v)"@ evaluates to true. @@ -286,6 +288,14 @@ doc /// x = hashTable{(1, 2), (2, 4), (3, 6), (4, 8), (5, 10)} selectPairs(x, (k,v) -> odd(k + v)) selectPairs(2, x, (k, v) -> odd(k + v)) + Text + If @CODE "x"@ is not a hash table, then @M2CODE "select(pairs x, f)"@ + (or @M2CODE "select(n, pairs x, f)"@) is called. + Example + selectPairs(toList(1..10), (i, x) -> even x) + selectPairs(3, toList(1..10), (i, x) -> even x) + Caveat + If @CODE "x"@ is a hash table, then it must be immutable. SeeAlso selectValues selectKeys diff --git a/M2/Macaulay2/packages/Macaulay2Doc/types.m2 b/M2/Macaulay2/packages/Macaulay2Doc/types.m2 index 002f784788..da6c6c40ce 100644 --- a/M2/Macaulay2/packages/Macaulay2Doc/types.m2 +++ b/M2/Macaulay2/packages/Macaulay2Doc/types.m2 @@ -544,7 +544,10 @@ document { } document { - Key => {hashTable,(hashTable, List)}, + Key => { + hashTable, + (hashTable, BasicList), + (hashTable, Function, BasicList)}, Headline => "make a hash table", TT "hashTable(h,v)", " -- produce a hash table from a list ", TT "v", " of key-value pairs, with an optional collision handler function ", TT "h", ".", PARA{}, diff --git a/M2/Macaulay2/tests/normal/hashtables.m2 b/M2/Macaulay2/tests/normal/hashtables.m2 index b194be50b4..d3e29d3b98 100644 --- a/M2/Macaulay2/tests/normal/hashtables.m2 +++ b/M2/Macaulay2/tests/normal/hashtables.m2 @@ -40,6 +40,8 @@ x = set(1..10) assert(select(x, odd) === set {1, 3, 5, 7, 9}) assert(select(2, x, odd) == set {1, 3}) +assert(hashTable ((0, 1), (1, 2)) === hashTable {(0, 1), (1, 2)}) + -- Local Variables: -- compile-command: "make -C $M2BUILDDIR/Macaulay2/packages/Macaulay2Doc/test hashtables.out" -- End: diff --git a/M2/Macaulay2/tests/normal/iterators.m2 b/M2/Macaulay2/tests/normal/iterators.m2 index fe741f7f79..340e14c4aa 100644 --- a/M2/Macaulay2/tests/normal/iterators.m2 +++ b/M2/Macaulay2/tests/normal/iterators.m2 @@ -52,3 +52,13 @@ assert Equation(toList(iterator {1, 2, 3} | iterator {4, 5, 6}), assert Equation(join({1, 2, 3}, iterator {4, 5, 6}), {1, 2, 3, 4, 5, 6}) assert Equation(join((1, 2, 3), iterator {4, 5, 6}), (1, 2, 3, 4, 5, 6)) assert Equation(join([1, 2, 3], iterator {4, 5, 6}), [1, 2, 3, 4, 5, 6]) + +assert Equation(toList pairs "foo", {(0, "f"), (1, "o"), (2, "o")}) +assert Equation(toList applyPairs("foo", (i, c) -> (c, i + 1)), + {("f", 1), ("o", 2), ("o", 3)}) +x = 0 +scanPairs("foo", (i, c) -> x += i + first ascii c) +assert Equation(x, 0 + 102 + 1 + 111 + 2 + 111) +assert Equation(selectPairs(1..10, (i, x) -> even x), + ((1, 2), (3, 4), (5, 6), (7, 8), (9, 10))) +assert Equation(selectPairs(2, 1..10, (i, x) -> even x), ((1, 2), (3, 4)))