Showing posts with label lang. Show all posts
Showing posts with label lang. Show all posts

Thursday, June 18, 2015

Using C++11 in Travis CI

There are many strange solutions, but there is now a better way to do this.

    sudo: false
    language: cpp
    compiler:
      - gcc
      - clang
    install:
    - if [ "$CXX" = "g++" ]; then export CXX="g++-4.8" CC="gcc-4.8"; fi
    addons:
      apt:
        sources:
        - ubuntu-toolchain-r-test
        packages:
        - gcc-4.8
        - g++-4.8
        - clang

The explicit sudo: false will let it build in Docker (for speed) even if you have a pre-docker repo, according to Travis support.

Thanks to solarce at Travis support for noticing my error and fixing the docs.




Sunday, February 15, 2015

Google C++ Style Guide

  • https://www.youtube.com/watch?v=NOCElcMcFik (Titus Winters at CppCon)
I am a big fan of this, mainly because I like to see side-effects clearly identified. There is an amazing amount of resistance. I don't think that the opposition can be moved, but maybe this reddit comment will at least convince managers of its value:

ericanderton 20 points  
My $0.02:
I've done a lot of work using this style guide religiously, largely because it was what my team came together over. It was also the most cogent guide we could find online that was more than merely prescriptive. It's exactness was the overall deciding factor.
I hated using it at first. Overall, the guide is very regressive, and chops the legs off of C++ such that it's not much more than "C with namespaces and classes." You could even go as far as to say that most arguments in the guide reduce to "this isn't a problem in Java or Python because you can't make this mistake, so don't use feature X here at all." Ultimately, it keeps you from doing anything that would allow bugs to creep in by mistake or by misinterpretation, by keeping it all nice and simple. And this is where this style guide actually helps.
The problem is that C++ provides almost too much leverage. Left unchecked, developers will likely use the language to its limits, which inevitably will confound other members of the team. While the program may be a masterpiece of template code, move semantics, and other concepts, it's now unmaintainable by anyone but the original author. Business wise, it's vastly preferable to have an uninspired piece of software if it means you can fix bugs while half your staff has the flu.
Also, consider that the goals of the Google C++ style guide align incredibly well with Go. To me, this is a very salient case against using C++ if that guide is at all a good fit for new development. Go has a very tight language spec, and a (IMO) superior concurrency model that is easier to construct, reason about, and debug. And it's still a compiled language.
Anyway, I'm proud to say that I have delivered excellent results, and relatively bug free code using this guide. If left to my own devices I probably would have used too much template magic and other mechanics that, while are all valid C++, would be harder to debug and understand by other members of my team. The resulting codebase is boring as all hell to read, but stable, reliable, and works incredibly well.
The downside is that compared to conventional languages, the result still takes a long time to compile, is twice as much code as is needed (header files), and relies heavily on smart pointers to manage memory (may as well use a GC). Again, this is why I mentioned using Go earlier.
One last thing: this guide does not stress the importance of "const correctness" in class construction. Add that to your work and you'll really have some solid code to rely upon.
tl;dr: For new development, either forgo this guide completely, or just use Go. Otherwise, you'll just piss off experienced C++ developers by using this thing.
Edit: I forgot to mention that the GSG has a massive blind spot for exception safety. Just because your code doesn't use exceptions, doesn't mean that the libraries you use don't throw. This includes the STL; the guide should steer you away from throwing variants of STL functions/methods, but it doesn't. So be on the lookout for coders not throwing try/catch around third party code, and refusing to use basic RAII (also not mentioned in the guide) to make things airtight. Either that, or just except that every exception-based runtime fault should halt the program, and that it's an acceptable failure mode (probably not).
For a longer discussion, see:



Wednesday, January 7, 2015

C++: STL Iterator confusion

Recently, folks at my last company got confused with STL iterators.
  1. They used boost::python to create an InputIterator, using stl_iterator.
  2. They used is_sorted() to check whether the underlying list was sorted (of course).
  3. They then looped over their iterator.
That was a mistake. An InputIterator is single-pass, so someone obviously forgot to read the documentation.

But this highlights a problem with STL. is_sorted() actually requires a ForwardIterator. At that link, you can see that a ForwardIterator might be "mutable", meaning that it can be written into. But is_sorted() takes a copy of an iterator, and it certainly does not write into it. There can be no issue of side-effects, right?

Wrong. You can pass an InputIterator if you want, at your own risk. is_sorted() might still return the correct result, but the original iterator (which was copied for the call to is_sorted()) might now be invalid.

So the question became whether it was safe to pass an Inputerator to is_sorted(). In general, the answer is "No". Look at representative source code:

template <class ForwardIterator>
  bool is_sorted (ForwardIterator first, ForwardIterator last)
{
  if (first==last) return true;
  ForwardIterator next = first;
  while (++next!=last) {
    if (*next<*first)     // or, if (comp(*next,*first)) for version (2)
      return false;
    ++first;
  }
  return true;
}

That function requires a copy of the original "first" iterator. "first" and "next" must both be valid during the entire function, and that is not guaranteed for an InputIterator.

However, the boost::python InputIterator is actually a memoized iterator. If you increment it, then the next value for any copy will no longer be the value after what the copy points to. But the copy remains valid for reference. So is_sorted() is fine, as long as no further iteration is performed later.

The problem is that STL is missing a concept between ForwardIterator and InputIterator. So is_sorted() is given the more restrictive designation.

Another way to look at this is that C++ templates are insufficient for practical iterators. The following is closer to the function we really want:

template <class ForwardIterator>
  bool is_sorted (ForwardIterator first, ForwardIterator last)
{
  if (first==last) return true;
  ForwardIterator next = first;
  auto prev_value = *first;
  while (++next!=last) {
    auto next_value = *next;
    if (next_value<prev_value);
      return false;
    prev_value = next_value;;
  }
  return true;
}

But that has problems too. Suddenly, the underlying type must be copy-constructible, and those copies could be expensive. We could use a pointer, but a pointer to what? If we don't make a copy, then there is nothing to point to.

We need an extra version of this algorithm which can safely accept InputIterators, with the implication that there will be copies of the underlying data. But it would need a different function name, and it would confuse everyone.

What we really need is the concept of "Iterator with invariant current element (unless underlying container is modified) but possibly variant subsequent Iterator". Lots of iterators actually work that way. But it would probably confuse people even more.

Friday, September 12, 2014

C++: copy elision

The C++ standard permits the compiler to skip (or "elide") a constructor under some circumstances, e.g. when a temporary is passed into a function by value. There is plenty of information on the web about this subject, but here is a concrete example.

What's interesting is that "copy elision" can actually alter semantics:
When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the copy/move constructor and/or destructor for the object have side effects.
Anyway, because of this feature, it can be wise to let a method accept an input by value rather than by const-reference, e.g.
table[key] = value;
The pattern should be considered whenever the method would have created an explicit copy, as for swapping.

Friday, August 15, 2014

Recursive memoization with Go

There are many odd solutions on the web to the "Web Crawler" exercise in the GoLang tutorial:
    https://tour.golang.org/concurrency/9
    http://soniacodes.wordpress.com/2011/10/09/a-tour-of-go-69-exercise-web-crawler/#comment-176

It's presented as a recursive function which requires memoization and parallelism. There are 2 main approaches:
  1. Synchronization primitives, or
  2. Callbacks
Both are needlessly complex. Here is my solution:
    https://gist.github.com/cdunn2001/a0caf94ce6c5f1da002b
 
Mine is simple because I dropped the recursion. If you see a producer/consumer pattern with recursion, you probably need synchronization primitives. But if you can drop the recursion, then the consumer can know exactly how many producers he created.

An interesting sidebar is how to memoize in Go.  A closure is probably the best way, but I didn't bother with that in my solution, to minimize diffs.

For another angle on the deceptive simplicity of gochannels, read this:
    http://www.jtolds.com/writing/2016/03/go-channels-are-bad-and-you-should-feel-bad/

Sunday, March 31, 2013

C: Computed gotos vs. switch statement

A fairly new feature of some C compilers is the "computed goto", which is around 20% faster than a traditional switch-statement.

http://eli.thegreenplace.net/2012/07/12/computed-goto-for-efficient-dispatch-tables/

That's a great description, with examples, benchmarks, and an explanation of why it's faster. I'm recording that link here because I often forget the *name* of this thing. If you don't know what you're looking for, it's hard to find.

Sunday, July 24, 2011

Java: The worst part? Checked exceptions.

This guy hates checked exceptions in Java, with good reason. They are also a bad idea in C++. Bruce Eckel has chimed in as well.

A "checked" exception is one which is named in a "throws" annotation on a function.

void foo() throws MyException {}

Monday, July 11, 2011

C++0x: Does it have closures?

No. It has downward funargs, but not upward. More discussion is here.

This means that something in the style of node.js would be a bit more complicated in C++ (or Java, etc.) than in JavaScript/Perl/Python/Ruby/Lua/Go, etc.

Parameterized tests

There was a reddit discussion on assert vs. UnitTest-style assert_equal etc.

I really have no preference between assert x == y and assert_equal(x, y), given pytest's helpful tracebacks.

My problem with pytest is the awkward support for parameterized tests.  nose handles parameterized tests much better. (pytest handles the nose-style too, but that's hard to find in the docs.) This is an open issue for unittest2.
In a nutshell, when I test "purely functional" code (i.e. free of side-effects, referentially transparent) I want this:
  • To list inputs and correct outputs.
  • To apply each input to a specific function.
  • To consider each a separate testcase, so that they will all run even when one fails.
  • To learn which inputs failed, along with expected and actual results.
(ToDo: Write an example.)
GoogleTest (C++, not Python) has very good support for parameterized tests via TEST_P. However, GoogleTest does not allow TEST_P to be combined with TEST_F (parameterized within a fixture). That is something pytest allows, with a bit of work. 

Sunday, July 10, 2011

Concurrency in node.js: Objects vs. Functions -- or maybe both!

Here is an excellent article on using node.js as a simple web-server. In particular, it talks about dependency injection (with a reference to Martin Fowler's article) to handle routing, and it includes an aside on "nouns vs. verbs". Kiessling's aside refers to Yegge's 2006 article, which shows why Java is so verbose. As Yegge says,
I've really come around to what Perl folks were telling me 8 or 9 years ago: "Dude, not everything is an object."
All the talk of imperative vs. functional code -- and message-passing vs. function-passing -- seems to miss the point: We need both objects and functions!

Functional code facilitates multiprocessing by reducing dependencies. For example:
function upload() {
  console.log("Request handler 'upload' was called.");
  return "Hello Upload";
}
Except for the log message, that is functional code, which might be part of a web-server. Maybe a URL like "http://foo.com/upload" would eventually lead to this function. A more complex version of it could produce a whole web-page.

At first, this seems pleasantly scalable, but looks are deceiving. Consider how it might be called:
function route(pathname) {
  console.log("About to route a request for " + pathname);
  if (pathname == "/upload") {
    return upload();
  } else {
    console.log("No request handler found for " + pathname);
    return "404 Not found";
  }
}

function onRequest(request, response) {
  var pathname = url.parse(request.url).pathname;
  console.log("Request for " + pathname + " received.");

  response.writeHead(200, {"Content-Type": "text/plain"});
  var content = route(pathname)
  response.write(content);
  response.end();
}
The problem is that the entire stack -- from onRequest() to route() to upload() -- may block the server. The author of node.js, Ryan Dahl, has given many talks in which he discusses the importance of non-blocking calls for the sake of concurrency. Here is an example that can be non-blocking:
function upload(response) {
  console.log("Request handler 'upload' was called.");
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.write("Hello Upload");
  response.end();
}

function route(pathname, response) {
  console.log("About to route a request for " + pathname);
  if (pathname == '/upload') {
    upload(response);
  } else {
    console.log("No request handler found for " + pathname);
    response.writeHead(404, {"Content-Type": "text/plain"});
    response.write("404 Not found");
    response.end();
  }
}

function onRequest(request, response) {
  var pathname = url.parse(request.url).pathname;
  console.log("Request for " + pathname + " received.");
  route(pathname, response);
}
Notice that we are passing the response object from function to function. That is message-passing. The handler eventually writes directly into that object, rather than returning a string. Thus, the handler has side-effects. It is no longer functional code. But because it will be passed everything it needs, we can forget the call stack. Why is this an advantage? Because it allows us to use a cheap event loop. Let's suppose that upload() is a time-consuming operation:
function upload(response) {
  console.log("Request handler 'upload' was called.");

  exec("slow-operation", function (response, error, stdout, stderr) {
    response.writeHead(200, {"Content-Type": "text/plain"});
    response.write(stdout);
    response.end();
  });
}
The exec() call is slow, but exec() itself is non-blocking. When called, a sub-process starts, the function goes into the event queue, and upload() returns immediately. Thus, the system-call is executed concurrently with other operations. That's the essence of node.js.

To be clear, message-passing is not the main point. The response object could be stored temporarily in a closure, which is typical in JavaScript. (In fact, exec() in node.js does not actually allow response to be passed to its function.) We do not need a mutex lock on the response object because this is a single-threaded program, but even that's not the point. We could have multiple threads and lock the response object. It's in-memory, so response.write() is very fast. We create new events (with threads, processes, or whatever) only for slow (aka blocking) operations, and we assign call-backs to those events so that processing can be deferred. This is a paradigm which makes concurrency simple.

Friday, July 8, 2011

GoLang: I've created a new 'brush' for SyntaxHighlighter


I created a SyntaxHighlighter file for Go. (To set up SyntaxHighlighter, refer to this.)

This example is from the Go website.

// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

/*
   Write and http server to present pages in the file system, but
   transformed somehow. Substitutable? With Fibonacci program?
*/
package main

import (
 "bytes"
 "expvar"
 "flag"
 "fmt"
 "http"
 "io"
 "log"
 "os"
 "strconv"
)


// hello world, the web server
var helloRequests = expvar.NewInt("hello-requests")

func HelloServer(w http.ResponseWriter, req *http.Request) {
 helloRequests.Add(1)
 io.WriteString(w, "hello, world!\n")
}

// Simple counter server. POSTing to it will set the value.
type Counter struct {
 n int
}

// This makes Counter satisfy the expvar.Var interface, so we can export
// it directly.
func (ctr *Counter) String() string { return fmt.Sprintf("%d", ctr.n) }

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 switch req.Method {
 case "GET":
  ctr.n++
 case "POST":
  buf := new(bytes.Buffer)
  io.Copy(buf, req.Body)
  body := buf.String()
  if n, err := strconv.Atoi(body); err != nil {
   fmt.Fprintf(w, "bad POST: %v\nbody: [%v]\n", err, body)
  } else {
   ctr.n = n
   fmt.Fprint(w, "counter reset\n")
  }
 }
 fmt.Fprintf(w, "counter = %d\n", ctr.n)
}

// simple flag server
var booleanflag = flag.Bool("boolean", true, "another flag for testing")

func FlagServer(w http.ResponseWriter, req *http.Request) {
 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
 fmt.Fprint(w, "Flags:\n")
 flag.VisitAll(func(f *flag.Flag) {
  if f.Value.String() != f.DefValue {
   fmt.Fprintf(w, "%s = %s [default = %s]\n", f.Name, f.Value.String(), f.DefValue)
  } else {
   fmt.Fprintf(w, "%s = %s\n", f.Name, f.Value.String())
  }
 })
}

// simple argument server
func ArgServer(w http.ResponseWriter, req *http.Request) {
 for _, s := range os.Args {
  fmt.Fprint(w, s, " ")
 }
}

// a channel (just for the fun of it)
type Chan chan int

func ChanCreate() Chan {
 c := make(Chan)
 go func(c Chan) {
  for x := 0; ; x++ {
   c <- x
  }
 }(c)
 return c
}

func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 io.WriteString(w, fmt.Sprintf("channel send #%d\n", <-ch))
}

// exec a program, redirecting output
func DateServer(rw http.ResponseWriter, req *http.Request) {
 rw.Header().Set("Content-Type", "text/plain; charset=utf-8")
 r, w, err := os.Pipe()
 if err != nil {
  fmt.Fprintf(rw, "pipe: %s\n", err)
  return
 }

 p, err := os.StartProcess("/bin/date", []string{"date"}, &os.ProcAttr{Files: []*os.File{nil, w, w}})
 defer r.Close()
 w.Close()
 if err != nil {
  fmt.Fprintf(rw, "fork/exec: %s\n", err)
  return
 }
 defer p.Release()
 io.Copy(rw, r)
 wait, err := p.Wait(0)
 if err != nil {
  fmt.Fprintf(rw, "wait: %s\n", err)
  return
 }
 if !wait.Exited() || wait.ExitStatus() != 0 {
  fmt.Fprintf(rw, "date: %v\n", wait)
  return
 }
}

func Logger(w http.ResponseWriter, req *http.Request) {
 log.Print(req.URL.Raw)
 w.WriteHeader(404)
 w.Write([]byte("oops"))
}


var webroot = flag.String("root", "/home/rsc", "web root directory")

func main() {
 flag.Parse()

 // The counter is published as a variable directly.
 ctr := new(Counter)
 http.Handle("/counter", ctr)
 expvar.Publish("counter", ctr)

 http.Handle("/", http.HandlerFunc(Logger))
 http.Handle("/go/", http.StripPrefix("/go/", http.FileServer(http.Dir(*webroot))))
 http.Handle("/flags", http.HandlerFunc(FlagServer))
 http.Handle("/args", http.HandlerFunc(ArgServer))
 http.Handle("/go/hello", http.HandlerFunc(HelloServer))
 http.Handle("/chan", ChanCreate())
 http.Handle("/date", http.HandlerFunc(DateServer))
 err := http.ListenAndServe(":12345", nil)
 if err != nil {
  log.Panicln("ListenAndServe:", err)
 }
}

Embedding code with SyntaxHighlighter

Alex Gorbatchev's SyntaxHighlighter is the best thing around for adding syntax highlighting to code embedded into your blogs. Readers see line numbers but can easily cut-and-paste with or without those often pesky line numbers.

After you have all the necessary JavaScript and CSS files loaded into your web-page (see below for details on that) you have two choices for wrapping your source code. The "pre" method is necessary for CSS feeds, but the "script/CDATA" method handles embedded HTML tags without escaping them. The title is optional for both.

<script type="syntaxhighlighter" class="brush: js"><![CDATA[
  /**
   * SyntaxHighlighter
   */
  function foo()
  {
      convert("<body>Hello</body>");
      if (counter <= 10)
          return;
      // it works!
  }
]]></script>

Examples:
print "Hallo"



If you are using Google's Blogger (aka 'blogspot') as I am, you can prepare for syntax highlighting all blog posts via your Design template. Here are the steps:

  1. Click 'Design' at the top of your blogspot page.
  2. Click 'Edit HTML' at the top of that page.
  3. Find the tag which ends the HEAD section, and before the end insert the following:

<head>
...
<link href='http://alexgorbatchev.com/pub/sh/current/styles/shThemeDefault.css' rel='stylesheet' type='text/css'/>
<link href='http://alexgorbatchev.com/pub/sh/current/styles/shCore.css' rel='stylesheet' type='text/css'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shCore.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushJScript.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushPython.js' type='text/javascript'/>


<script type='text/javascript'>
  SyntaxHighlighter.config.bloggerMode = true;
  SyntaxHighlighter.all();
</script>

</head>

Add brushes for everything you might use, or try the new autoload feature of version 3.0. Then, you can use the pre or script/CDATA blocks as I've shown at the top of this page. If you need more help, follow these directions. Good luck!

Monday, April 18, 2011

Java: Are strings really immutable?

No, but a Security Manager can at least spot their mutations.

http://directwebremoting.org/blog/joe/2005/05/26/1117108773674.html

jQuery: "return false"

Back from vacation, I found this fascinating explanation of a common mis-use of jQuery:

http://fuelyourcoding.com/jquery-events-stop-misusing-return-false/

Wednesday, February 23, 2011

Go: Race conditions

Race conditions should be impossible in Go. Am I wrong?

Here is a verbose article on the subject. I'm not sure I get the point of the author, but the comments are interesting.

Thursday, January 6, 2011

jQuery: WTF?

Unbelievable.

I definitely prefer Prototype. Still, jQuery is much better than ASP. (Fortunately, the folks at MS recognized this, and VS MVC has supported jQuery since 2008.)

Thursday, December 30, 2010

Ruby: Inconsistencies

This is a growing list:
  • String#concat
    • Should be String#concat!
    • I guess it's meant to resemble #insert, #delete, #fill, #replace, #clear, etc., but I wish those had "!" too, like #compact!, #reject!, #slice!, etc. There should be 2 versions of all these, but because there is no "!", the non-mutating versions can never be.
  • Hash#update is a synonym for Hash#merge!
    • I have to remember which one has the "!"
    • Hash#invert does *not* mutate. How do I remember all this?
    • Even worse, Set#merge (no "!") mutates, unlike Hash#merge.
  • Set#add? and Set#delete?
    • These mutate.
  • s.chomp!.upcase
    • Can fail, since String#chomp! can return nil
  • Array#fetch(i) (and Hash#fetch(k)) can raise IndexError
    • Should be Array#fetch!(i)
    • Block and multi-arg version could drop the "!"
  • String#each does not exist in Ruby1.9, while #each_char does not exist in 1.8
    • We cannot write forward-compatible code!
    • Soln: s.split("")
  • '0x10'.hex and '0x10'.oct are same, but
    • '010'.hex and '010'.oct are different
  • Given: a = [1,2,3]
    • a[2] == 3
    • a[3] == nil
    • a[4] == nil
    • a[2,1] == [3] (and a[2,2] == [3] as well)
    • but a[3,1] == []
    • while a[4,1] == nil
  • For Arrays
    • a[x,y] = [nil] substitutes the slice
    • a[x,y] = z     substitutes [z], but ...
    • a[x,y] = nil   deletes the slice! (fixed in 1.9)
  • inspect/to_s
    • There used to be a clear distinction, like Python's __repr__/__str__, but 1.9 often (though not in all cases) erases that useful distinction.
  • Array   Float   Integer   String 
    • Those are Kernel functions, not constants.
  • ...
Also see Ruby Best Practices, a great book.

Wednesday, December 29, 2010

Ruby: Introspection

Supposedly, Ruby has introspection, but some things are missing. For example:

# How to get the name of the current method?
# Add this snippet of code to your logic somewhere
 
module Kernel
  private
  # Defined in ruby 1.9
  unless defined?(__method__)
    def __method__
      caller[0] =~ /`([^']*)'/ and $1
    end
  end
end
In Python, this is not much better:

import tracebackdef asdf():
    (filename,line_number,function_name,text)=traceback.extract_stack()[-1]
    print function_name
asdf()

Update: __method__ is part of Ruby as of 1.8.7.

Here is something else rather awkward in Ruby:

# Print all modules (excluding classes)
puts Module.constants.sort.select {|x| eval(x.to_s).instance_of? Module}
In Python, we could simply do this:
import sys; print sys.modules.keys()
Ruby's introspection (also) reveals a lot about the structure of classes.

Wednesday, December 8, 2010

JavaScript: Namespaces

http://javascriptweblog.wordpress.com/2010/12/07/namespacing-in-javascript/

The section on using this as a namespace proxy is brilliant. The origin of that idea is here (James Edwards).

Example:
var myApp = {};
(function() {
var id = 0;

this.next = function() {
return id++;
};

this.reset = function() {
id = 0;
}
}).apply(myApp)

window.console && console.log(
myApp.next(),
myApp.next(),
myApp.reset(),
myApp.next()
) //0, 1, undefined, 0
or more powerfully

var subsys1 = {}, subsys2 = {};
var nextIdMod = function(startId) {
var id = startId || 0;

this.next = function() {
return id++;
};

this.reset = function() {
id = 0;
}
};

nextIdMod.call(subsys1);
nextIdMod.call(subsys2,1000);

window.console && console.log(
subsys1.next(),
subsys1.next(),
subsys2.next(),
subsys1.reset(),
subsys2.next(),
subsys1.next()
) //0, 1, 1000, undefined, 1001, 0

Sunday, November 21, 2010

Ruby: Whitespace significant?

Many people claim that they do not like Python because of the significance of whitespace. Well, whitespace is also significant in Ruby. E.g.
>> def say
>>  puts 'hi'
>> end
=> nil
>> say
hi
=> nil
>> def say puts 'hi' end
SyntaxError: (irb):5: syntax error, unexpected tSTRING_BEG, expecting ';' or '\n'
def say puts 'hi' end
              ^
(irb):5: syntax error, unexpected keyword_end, expecting $end
        from /Users/cdunn2001/bin/irb:12:in `
' >> def say; puts 'hi'; end => nil >> say hi => nil
See? The newline is a substitute for the semicolon, not equivalent to a space or tab. This is not pedantry. To me, the main value of whitespace-independence is that you can insert the code into something else -- e.g. an HTML template -- without breaking it.

I wouldn't mind if I could use curly brackets instead of 'end', but that works only for blocks, not function definitions. So I wish that Ruby fans would quit bragging that their language is whitespace-independent. There are such languages, e.g. Perl, where whitespace merely delimits tokens and can be removed completed by relying on parentheses and other delimiters. Ruby is not one of them.

I do understand the objection to Python's syntax. It's not the enforced indentation; it's the lack of an 'end' delimiter. The result is that copy-and-paste operations can introduce mistakes. I get that.

I'm just sayin' ...