8

Correct usages of Character/isWhitespace include:

(Character/isWhitespace \a) => false
(Character/isWhitespace \ ) => true

However, my first attempt was this, and I find the error confusing.

(Character/isWhitespace "")
  =>  IllegalArgumentException No matching method found: isWhitespace
  => clojure.lang.Reflector.invokeMatchingMethod (Reflector.java:80)

The IllegalArgument part makes sense, but why does it say "no matching method found"? Clearly the function does exist.

Clarification

The reason I'm asking this question is that I'm new to Clojure, and think I'm fundamentally misunderstanding something.

When I type (Character/isWhitespace \a), what I think I'm saying is: "I know that there's a Character namespace, and within that there's a function called isWhitespace, and I want to call that function and pass in \a".

On this mental model, my results above are confusing because it seems like Clojure is saying, "whenever you give me an argument type that this function doesn't accept, I'm going to pretend the function doesn't exist." Eg, "you're not allowed to blend bricks, so if you try, I'm going to give you a BlenderDoesntExist error." Which is weird.

Some answers seem to imply that

  • The name Character/isWhitespace is only part of what Clojure uses to look up the function, and the other part is the type of the argument. (I've done some more searching: is this maybe a multimethod?)
  • The method is being looked up on a Java class?

A great answer would clarify this process for me.

Nathan Long
  • 122,748
  • 97
  • 336
  • 451
  • 1
    I guess because there is no `isWhitespace` that accepts `String`. – BahmanM Oct 25 '13 at 12:54
  • added a compiler walkthrough dot macro interpretation process, let me know if is it any clear – guilespi Oct 25 '13 at 14:04
  • 1
    namespaces / functions usually don't start with caps and aren't spelled camelCase. You can differentiate a true clojure function from a java method by trying to use it as a standalone value at the repl - you will get an error for methods, for functions it will show an internal representation of the function. – noisesmith Oct 25 '13 at 14:45

4 Answers4

10

tl;dr

The clojure compiler relies on reflection in order to find matching signatures for Java interop Class methods, and it's raising it's own exceptions when nothing is found.

In this case an IllegalArgumentException is correctly raised but noMethodReport error message is displayed which leads to confusion.

And this is the source code responsible for it on the clojure github repo.

The long version

First, Java interop parsing walkthrough.

When the clojure parser finds the . dot macro the HostExpr parser handles the parsing, which tries to decide whether second argument is a symbol or a Class.

If it's a class assumes it's a static method of that class being called, and parsing continues on StaticMethodExpr.

First thing inside parser is try to find the method by reflection on the class:

  List methods = Reflector.getMethods(c, args.count(), methodName, true);
  if(methods.isEmpty())
      throw new IllegalArgumentException("No matching method: " + methodName);

Which it properly finds and no exception is raised at that point

Then it adds parameters to the found methods:

  java.lang.reflect.Method m = (java.lang.reflect.Method) methods.get(i);
  params.add(m.getParameterTypes());

After that tries to find the matching method signature index:

  methodidx = getMatchingParams(methodName, params, args, rets);

Which for this case returns '-1' and method stays null. And that's for the parsing stage.

Evaluation time...

Then when invokeStaticMethod is called, it calls getMethods on Reflector.java which properly find two matching signatures for 'isWhitespace'.

And finally the confusing message you're seeing, inside function:

 static Object invokeMatchingMethod(String methodName, List methods, Object target, Object[] args)

Found methods are tested for parameter matching trying to find a method with the proper signature:

 for(Iterator i = methods.iterator(); i.hasNext();)
   {
    m = (Method) i.next();
    Class[] params = m.getParameterTypes();
    if(isCongruent(params, args))

If no matching signature is found an exception is raised

if(m == null)
   throw new IllegalArgumentException(noMethodReport(methodName,target));

So the answer would be that the clojure compiler relies on reflection in order to find matching signatures for the methods, and it's raising it's own exceptions when nothing is found.

In this case an IllegalArgumentException is correctly raised but noMethodReport error message is displayed which leads to confusion.

Community
  • 1
  • 1
guilespi
  • 4,672
  • 1
  • 15
  • 24
8

Character/isWhitespace is a Java method (a static method of the java.lang.Character class). See Java interop for examples of syntax that call Java methods.

Whereas regular Clojure functions are defined by their name alone, Java methods are defined by their signature which consist of their name and the number and types of their parameters.

The only variants of the isWhitespace method defined in the Character class are isWhitespace(char ch) and isWhitespace(int codepoint). So there's "no matching method" for calling isWhitespace with a string.

Rörd
  • 6,556
  • 21
  • 27
  • this does not explain why the message is "no method found" instead of "wrong arguments" – guilespi Oct 25 '13 at 18:33
  • @GuillermoWinkler: Yes, I explained the reasoning for the problem but forgot to explicitly answer the question. I've added a paragraph addressing that now. – Rörd Oct 25 '13 at 23:58
3

Because "" is a type string, not character.

    user=> (type \a)
    java.lang.Character
    user=> (type "")
    java.lang.String

The string (java) class do not have the "isWhitespace" method while the "character" one does. You can't drive a nail with a screwdriver.

There is some hints there:

How to represent empty char in Java Character class

The "" is an empty string. But there's no representation of an "empty" character. Although the Character "\0" does:

      user=> (Character/isWhitespace \0)
      false

And the isWhitespace character's doc:

http://docs.oracle.com/javase/6/docs/api/java/lang/Character.html#isWhitespace%28char%29

Says that:

"The set of characters from U+0000 to U+FFFF is sometimes referred to as the Basic Multilingual Plane (BMP). Characters whose code points are greater than U+FFFF are called supplementary characters. The Java 2 platform uses the UTF-16 representation in char arrays and in the String and StringBuffer classes. In this representation, supplementary characters are represented as a pair of char values, the first from the high-surrogates range, (\uD800-\uDBFF), the second from the low-surrogates range (\uDC00-\uDFFF)." to which empty quote does not fall into.

In other words, the isWhitespace method has no way to "understand" an empty string.

Furthermore, from the clojure java interlop doc http://clojure.org/java_interop :

The preferred idiomatic forms for accessing field or method members are given above. The instance member form works for both fields and methods. They all expand into calls to the dot operator (described below) at macroexpansion time.

so typing

      (Character/isWhitespace "")

expands to

       (. Classname instanceMember instance args*) 

       (. Character isWhitespace "")

causing the error.

       user=>  (. Character isWhitespace "")
       java.lang.IllegalArgumentException: No matching method found: isWhitespace (NO_SOURCE_FILE:0)

While you would have to look at clojure source code to confirm, my guess is that is comes from the dynamic java compiling that might go underneath:

  public class toto {

     public toto ()
     {
        Character.isWhitespace("");
     }

  }

then:

  javac toto.class

  toto.java:5: error: no suitable method found for isWhitespace(String)
  Character.isWhitespace("");
         ^
  method Character.isWhitespace(int) is not applicable
  (actual argument String cannot be converted to int by method invocation conversion)
  method Character.isWhitespace(char) is not applicable
  (actual argument String cannot be converted to char by method invocation conversion)
  1 error
Community
  • 1
  • 1
Sebastien
  • 1,439
  • 14
  • 27
  • That's like saying "The reason I told you the blender doesn't exist is because you wanted to blend a brick." The correct error would be "you're not allowed to blend a brick." – Nathan Long Oct 25 '13 at 12:59
  • 1
    The macro expansion you suggest is wrong: (macroexpand '(Character/isWhitespace String)) => (. Character isWhitespace String) – guilespi Oct 25 '13 at 13:28
  • Anyway I think your answer does not answer the question, the question is why the dot macro doesn't fail with an invalid argument but with an invalid method. – guilespi Oct 25 '13 at 13:39
  • "You can't drive a nail with a screwdriver." Right. But my confusion is that, if I say `(Screwdrive nail)`, the error I get isn't "the Screwdrive function doesn't accept nails", but "Screwdrive doesn't exist." I know it exists, because I've done `(Screwdrive screw)` successfully. – Nathan Long Oct 25 '13 at 13:41
  • @Nathan Clojure is a functional programming shellac over an OO language. Error messages are one of those places you can still see the OO shining through. `scerw.Screwdrive()` may exist but `nail.Screwdrive()` doesn't thus the error message. – stonemetal Oct 25 '13 at 14:00
  • I think all elements from my answer covers everything. If you guys thinks the preamble is confusing, I can skip it. The thing is: 1) . is a special form and 2) the runtime error comes from it's resolution within the interpreter. 3) the programming error comes from a misuse of the "Character.isWhitespace" method/class. – Sebastien Oct 25 '13 at 14:01
2

There are no method like isWhitespace in Character api which accepts String as argument. isWhitespace method only takes int or char as the argument.

Abimaran Kugathasan
  • 31,165
  • 11
  • 75
  • 105
  • When I say `Character/isWhitespace`, am I not telling it exactly which method I want? Am I instead asking it to look up a method that will accept this type of argument? – Nathan Long Oct 25 '13 at 13:01
  • @NathanLong, Yes, Clojure will infer the correct method based on the passed argument. – Abimaran Kugathasan Oct 25 '13 at 13:03
  • 2
    @NathanLong, the important context here is that Java methods can be [overloaded](http://www.java-samples.com/showtutorial.php?tutorialid=284). – jbm Oct 25 '13 at 13:29
  • @jbm - Thanks, that's very helpful! I didn't know that I was calling a Java method, and I didn't know those could be overloaded. :) – Nathan Long Oct 25 '13 at 13:44
  • 1
    @NathanLong: Yes, that's the core oft he matter here. Unlike regular Clojure functions, Java methods aren't identified my their name alone, but by their full signature which consists of the method's name and the number and types if its parameters. – Rörd Oct 25 '13 at 14:19
  • @Rörd if you want to expand that into an answer, like, "the reason is that you're calling a Java method, and here's how you can tell, and Java is different in this way", I'll accept it. That was what I didn't know. – Nathan Long Oct 25 '13 at 16:45
  • @NathanLong: I've added an answer. It hope it's enough to clarify the matter despite its brevity. – Rörd Oct 25 '13 at 17:06