The Right Lang for the Job

Exploring the abbyss of non-mainstream programming languages

Supercharge your eval-expression with ielm! ¶

With some tinkering customization, ielm can be a capable replacement

Created: · Updated:

1. TLDR: why and when

I often use straight-visit-package and find-library to read the READMEs and ;; Commentary: sections in Elisp files. On top of that, I use Info quite extensively - the manuals for Elisp and Org Mode are loaded with tons of useful Elisp snippets.

The problem: while you can use C-x C-e almost everywhere, some of the places listed above are read-only buffers (Info, built-ins from /usr/lib/), so if I want to evaluate it in a slightly changed form (longer than one line, since that would fit in eval-expression), I need to switch to scratch buffer, which is pretty intrusive. With addition of IELM, my workflow looks like this:

  1. I'm reading code or docs in a read-only buffer.
  2. I find a piece of code I want to play with (modify and execute)
  3. If it's one line, I copy it to M-: (eval-expression)
  4. If it's up to 10 lines, I copy it to C-M-: (ielm)
  5. If it's longer, I copy it to *scratch* buffer

In this post, I focus on pt. 4.

UPDATE: Another kind soul on Reddit (/u/deaddyfreddy) pointed at edit-indirect package that could be a good replacement for this. I will take a look!

2. eval-expression / M-:

Bound under M-: by default, eval-expression allows you to quickly type any Elisp and execute it in the context of current window and buffer. I think it's one of my most used key bindings, after M-x. It supports TAB-completion, and even shows help (function and macro signatures).

screenshot_2023-12-04_2244.png

The input takes place in the minibuffer, which means it automatically keeps history of pevious invokations. You can search through that history, using your preferred completion styles, for example with Orderless (and Vertico, here) you can use regular expressions to get what you want:

screenshot_2023-12-04_2258.png

Unfortunately, the minibuffer is not the best way to input more complex forms. Multiline is possible, but tedious. Lack of enabled paredit hurts (and enabling it messes up incomplete input handling). For these reasons, for trying out longer pieces of code, I normally switch to the *scratch* buffer. The downside of it is that I need to switch to a buffer, often losing sight of the buffer I worked with before that.

Fortunately, there's a middle ground: ielm! It's documented with a single page in the Emacs manual and just a handful of customizable options. By default, as is usual for Emacs, it doesn't look or work too well, but with a bit of configuration, it gets a lot better. Here's how I use it.

3. ielm / C-M-:

First, let's make ielm look better. As a replacement for eval-expression it should display at the bottom of the frame. I also prefer it to reduce the clutter - the default headaer and the prompt could use some tweaking:

 1: (cl-defun my-make-right-arrow-icon ()
 2:   "Return a string that displays arrow icon  when inserted in a buffer."
 3:   (propertize (all-the-icons-octicon "arrow-right")
 4:     'face `(:family ,(all-the-icons-octicon-family) :height 1.2)
 5:     'display '(raise 0)))
 6: 
 7: ;; Make *ielm* buffer display in a side-window at the bottom of the frame
 8: (add-to-list 'display-buffer-alist
 9:              '("*ielm*"
10:                (display-buffer-in-side-window)
11:                (side . bottom)
12:                (window-height . 10)))
13: 
14: (setq ielm-header "")
15: (setq ielm-prompt (concat (my-make-right-arrow-icon) " "))
16: 
17: (keymap-global-set "C-M-:" #'ielm)

We use display-buffer-alist to add a rule for where and how *ielm* buffers should show up, then remove the header (why waste a line?) and change the prompt to a shorter one. We get this as a result:

screenshot_2023-12-05_2101.png

Now, let's make the *ielm* buffer behave more like a normal Elisp buffer. I have a lot of config already in place for Elisp, and since the buffer is not a minibuffer, most of these will just work. So, I add a few hooks:

 1: ;; *ielm* buffer
 2: (add-hook 'ielm-mode-hook 'my-elisp-mode-setup) ; (1)
 3: (add-hook 'ielm-mode-hook 'my-ielm-mode-setup)
 4: (keymap-set ielm-map "C-M-:" #'kill-buffer-and-window) ; (2)
 5: 
 6: ;; indirect buffer used by ielm to fontify elisp code
 7: (add-hook 'ielm-indirect-setup-hook #'rainbow-delimiters-mode) ; (3)
 8: 
 9: (defun my-ielm-mode-setup ()
10:   (paredit-mode)
11:   (keymap-local-set "C-<return>" #'my-ielm-send-input) ; (4)
12:   (let ((map (copy-keymap paredit-mode-map)))
13:     (keymap-set map "<RET>" 'ielm-return)
14:     (push `(paredit-mode . ,map) minor-mode-overriding-map-alist)))
15: 
16: (cl-defun my-ielm-send-input ()
17:   (interactive)
18:   ;; The pattern of: '<return> SHIT, I meant execute! C-/ C-<return>' repeats
19:   ;; iself often enough that automation seems warranted... :)
20:   (when (eq 'ielm-return last-command) (undo))
21:   (ielm-send-input))

my-elisp-mode-setup (at (1)) is a hook I run in normal Elisp buffers. It enables tens of packages and messes with a lot of keybindings, you can see it on Github. my-ielm-mode-setup only overrides some key bindings - paredit would normally hijack the RET key, so we use minor-mode-overriding-map-alist to tell it not to do that. With that, we have a nice multiline editing, structural editing with paredit, and fontification and indentation that work:

screenshot_2023-12-05_2116.png

With ielm-dynamic-multiline-inputs and ielm-dynamic-return set, we can now insert newlines normally as long as we're not at the very end of the input. Having to move point to the end to execute the code can be tedious, so I bound C-<return> to send the input immediately, no matter where the point currently is.

Since this is a normal Elisp buffer, we can also use C-M-x and C-x C-e to execute parts of the current input. That allows you to refine the input before actually executing it.

Going further, one of the nice things about eval-expression is that it works in the context of the current window and buffer. I didn't want to mess with how IELM executes its input; instead, I made a little helper function:

 1: (keymap-global-set "C-M-:" #'my-run-ielm)
 2: 
 3: (cl-defun my-run-ielm (arg)
 4:   (interactive "P")
 5:   (let ((buf (buffer-name (current-buffer))))
 6:     (ielm)
 7:     (when arg
 8:       (insert
 9:        (prin1-to-string
10:         (pcase arg
11:           ('(4) `(with-selected-window (get-buffer-window ,buf)))
12:           ('(16) `(with-current-buffer ,buf)))))
13:       (forward-char -1)
14:       (ielm-return))))

Now, when I want to execute some code in the context of a window or buffer I was before invoking IELM, I can press C-u C-M-: or C-u C-u C-M-:. This is what I get in that case:

screenshot_2023-12-05_2131.png

I can now use things like re-search-forward in the context of the window I was in before switching to IELM - and see the point move as the command gets executed.

Finally, remember the "help" that eval-expression offers? Since *ielm* is a normal buffer, it can do a lot more - for example, you can use it with Corfu:

screenshot_2023-12-05_2143.png

Of course, all the other goodies you have configured for emacs-lisp-mode will also work. This is the ultimate advantage of this solution over eval-expression, in my opinion.

4. Conclusion

One thing missing is saving and searching of the history of commands. It's kept in comint, I think, and is not persistent. Recalling previous commands in a single IELM session (with C-<up>) works, but C-r starts an isearch of the buffer instead of Orderless search with Vertico. This is something I still need to figure out.

UPDATE: a kind soul over at Reddit (/u/FrankLeeMadear) provided the missing piece for me! Thank you, I will use it well! 🙂

 1: (defvar ielm-input-ring nil
 2:   "Global to hold value of input ring. Add this to savehist-additional-variables")
 3: 
 4: (defun set-ielm-input-ring ()
 5:   ;; called only when ielm is current buffer
 6:   ;; init local var from global saved by savehist
 7:   (when ielm-input-ring
 8:     (setq comint-input-ring ielm-input-ring)))
 9: 
10: (defun save-ielm-input-ring ()
11:   (let* ((buf (get-buffer "*ielm*"))
12:          (ring (and buf (buffer-local-value 'comint-input-ring buf))))
13:     (when ring 
14:       (setq ielm-input-ring ring))))
15: 
16: (add-hook 'ielm-mode-hook 'set-ielm-input-ring)
17: (add-hook 'savehist-save-hook 'save-ielm-input-ring)
18: (add-to-list 'ielm-input-ring 'savehist-additional-variables)

Other than that, I think this config fits nicely between eval-expression and visiting an Elisp buffer (scratch or otherwise). I've been using this for a few weeks to experiment and play with new libraries. Being able to choose in which context the code will be executed is convenient, and the full power of structural editing and completion with Corfu even for throwaway snippets helps a lot when exploring and prototyping code.

Advent of Code 2023 - Day 4 ¶

I'll just stop adding "Part 1" to the title...

Created:

This puzzle was way too easy, compared to yesterday's. Again, I won't do part 2 due to lack of time - that's probably going to be the same for the following posts, too - but part 1 was trivial.

In this puzzle, we're asked to find the size of the set intersection of two lists of numbers. After that, we need to compute the following:

The first match makes the card worth one point and each match after the first doubles the point value of that card.

According to the explanation, the result for the example should be 13. Here's the solution:

 1: (defconst example-input
 2:   "Card 1: 41 48 83 86 17 | 83 86  6 31 17  9 48 53
 3: Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19
 4: Card 3:  1 21 53 59 44 | 69 82 63 72 16 21 14  1
 5: Card 4: 41 92 73 84 69 | 59 84 76 51 58  5 54 83
 6: Card 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36
 7: Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11")
 8: 
 9: (cl-defun parse-input (data)
10:   (cl-loop for card in (split-string data "\n")
11:            for (name scores) = (split-string card ": +")
12:            for nums = (split-string scores " +")
13:            collect (-map #'read nums)))
14: 
15: (let ((scores (--map (-partition-by #'numberp it) (parse-input example-input))))
16:   (cl-loop for (winning _ haves) in scores
17:            for win-expt = (1- (length (-intersection winning haves)))
18:            when (>= win-expt 0)        ; b/c `expt' works with negative numbers
19:            sum (expt 2 win-expt)))
13

It works! 🙂

Interesting points:

  1. We use read instead of string-to-number on line 13 because the separator (|) is a valid symbol, so we don't need to special-case it.
  2. The multiplication described in the task can be written as exponentiation with base 2, with exponent being one less than the number of winning numbers we have. However, expt works with negative exponents properly (instead of throwing an error or returning 0), so we need to filter out cases where none of the numbers we have are "winning". Otherwise, we'd get (expt 2 -1) == 0.5, which would be accumulated and we'd get the wrong result.
  3. As previously, split-string is the workhorse for parsing, and cl-loop along with a few functions from dash (esp. -partition-by and obviously -intersection) came in handy.

Part 2 is a little more interesting, but unfortunately, I have to go to work, so maybe next time…

Advent of Code 2023 - Day 3, Part 1 ¶

A classic, buffer-based implementation this time

Created:

I made it! I woke up early, drank 3 coffees, but in the end, I managed to solve the puzzle and post the solution before the others! 🙂

The puzzle this time is about extracting numbers from a 2D array of characters and checking whether the number neighbours any character other than a dot (".").

In Emacs, all text is in a 2D array by default - that's (conceptually) what a buffer is, after all. So there's really not much to the solution this time, other than handling some corner cases (first and last lines, and treating newlines as dots - for numbers that are at the end of the line).

Here's the solution:

 1: ;; NOTE: lines are 140 characters wide (maybe verify this instead of assuming?)
 2: (with-current-buffer (get-buffer "input3.txt")
 3:   (goto-char (point-min))
 4:   (let (numbers (lines (count-lines (point-min) (point-max))))
 5:     (while (re-search-forward (rx (1+ num)) nil t)
 6:       (let* ((beg (match-beginning 0))
 7:              (end (match-end 0))
 8:              (surrounding (concat 
 9:                            (string (char-after (1- beg))) ; preceding char
10:                            (string (char-after end))      ; following char
11:                            ;; previous line
12:                            (if (not (= 1 (line-number-at-pos)))
13:                                (buffer-substring-no-properties (- beg 142) (- end 140))
14:                              ".")
15:                            ;; next line
16:                            (if (not (= lines (line-number-at-pos)))
17:                                (buffer-substring-no-properties (+ beg 140) (+ end 142))
18:                              "."))))
19:         (unless (cl-every (lambda (c) (or (eq c ?\n) (eq c ?.))) surrounding)
20:           (push (string-to-number (match-string 0)) numbers))))
21:     (apply #'+ numbers)))
507214

We just loop through the numbers in the buffer, fetch the characters that surround the number, and verify there are only dots (and newlines, as a special case) in these characters.

I thought about using save-excursion and forward-line to get the preceding and following lines, then using looking-at-p to check for symbols - I think it would work and I wouldn't need to check line width in that case. By the time I thought of it, though, buffer-substring-based solution worked already, so I just left it as is.

Unfortunately, I don't have the time to solve Part 2 of the puzzle. In part 2, you need to find all * symbols that have more than one number in its surroundings. It's more interesting than Part 1 because there are more ways of solving it. I'll see if I have the time to code the solution in the evening.

Advent of Code 2023 - Day 2 ¶

Just Part 1, since I'm late to the party again today...

Created:

1. Part 1

Here's the link to the exercise - as usual, we get some input data, and need to read it and check some of the properties. In this case, the question is as follows:

Determine which games would have been possible if the bag had been loaded with only 12 red cubes, 13 green cubes, and 14 blue cubes.

Okay - before we start, let's import the libraries we'll need. Strictly speaking, none if these are actually needed - you can see other implementations that don't use anything outside of subr and simple - but I want to showcase the tools modern Elisper has at their disposal.

1: (require 'b)
2: (require 'cl-lib)
3: (require 'map)
4: (require 'dash)  

With that out of the way, we can take a stab at the task. First, let's assume we have the input in a buffer already, in input.txt. You could implement the task by imperatively traversing the buffer, which would be very Emacs-y way of doing this, but since there's an implementation like that already, I'll go for a more Python-like approach and split the input into a list of lines:

5: (defconst aoc-games-played-input
6:   (-> "input.txt" get-buffer b-string-no-properties s-trim s-lines))

We now have 100 lines of text, ready to be further processed:

(length aoc-games-played-input)
100

Here's how the lines look like:

(-take 2 aoc-games-played-input)
("Game 1: 12 blue, 15 red, 2 green; 17 red, 8 green, 5 blue; 8 red, 17 blue; 9 green, 1 blue, 4 red"
 "Game 2: 6 red, 6 blue, 2 green; 1 blue, 1 red; 6 green, 1 red, 10 blue")

We need to verify that on each line all the "subsets" - separated by semicolons - have color values that do not exceed the limits quoted at the beginning. First, let's further split the lines into chunks, separating game ID from subsets, and then further separating subsets from each other:

(let ((line (car aoc-games-played-input)))
  (pcase-let*
      ((`(,game ,subsets) (s-split (rx ": ") line))
       (subset-list (s-split (rx "; ") subsets))
       (subsets (--map (s-split (rx ", ") it) subset-list)))
    (cons
     (cl-second (s-match (rx "Game " (group (1+ num))) game))
     subsets)))
("1" ("12 blue" "15 red" "2 green") ("17 red" "8 green" "5 blue")
 ("8 red" "17 blue") ("9 green" "1 blue" "4 red"))

It works, so let's refactor it a bit before moving on. As we're doing the refactor, we can also convert strings to numbers and symbols as appropriate, and subsets into alists:

 7: (cl-defun get-game-id (game-name)
 8:   (let ((re (rx "Game " (group (1+ num)))))
 9:     (->> game-name (s-match re) cl-second string-to-number)))
10: 
11: (cl-defun subset->alist (colors)
12:   (cl-loop for color in colors
13:            for (num color) = (s-split (rx " ") color)
14:            collect (cons (intern-soft color) (string-to-number num))))
15: 
16: (cl-defun line->game (line)
17:   (pcase-let* ((`(,game ,subsets) (s-split (rx ": ") line))
18:                (subset-list (s-split (rx "; ") subsets))
19:                (subsets (--map (s-split (rx ", ") it) subset-list)))
20:     (cons
21:      (get-game-id game)
22:      (-map #'subset->alist subsets)))  )
23: 
24: (let ((line (car aoc-games-played-input)))
25:   (line->game line))
(1 ((blue . 12) (red . 15) (green . 2)) ((red . 17) (green . 8) (blue . 5))
   ((red . 8) (blue . 17)) ((green . 9) (blue . 1) (red . 4)))

So we have the data in a form that's convenient to work with. Now, what does it mean for a game to be possible? All subsets must have color values within a limit. Let's define a function for checking this:

26: (defconst limits '((red . 12) (green . 13) (blue . 14)))
27: 
28: (cl-defun game-possible-p (game)
29:   (cl-loop for subset in (cdr game)
30:            unless (cl-loop for (color . limit) in limits
31:                            always (<= (map-elt subset color 0) limit))
32:            return nil
33:            finally return t))
34: 
35: (defconst line-impossible
36:   "Game 1: 12 blue, 15 red, 2 green; 17 red, 8 green, 5 blue; 8 red, 17 blue; 9 green, 1 blue, 4 red")
37: (defconst line-possible
38:   "Game 2: 6 red, 6 blue, 2 green; 1 blue, 1 red; 6 green, 1 red, 10 blue")
39: 
40: (list (game-possible-p (line->game line-possible))
41:       (game-possible-p (line->game line-impossible)))
(t nil)

The predicate seems to be working. The two things left to do are to filter out the games for which the predicate returns t and then sum the game IDs of those games:

42: (cl-loop for line in aoc-games-played-input
43:          for game = (line->game line)
44:          if (game-possible-p game)
45:          sum (car game))
2716

And that's, according to the AoC page, the correct answer for my dataset, for part 1!

2. Part 2

For completeness sake, here's the answer to the second part of the puzzle. Here, we need to find the maximum values for each color across all subsets of a given game:

46: (defconst maxes '((red . 0) (green . 0) (blue . 0)))
47: 
48: (cl-macrolet
49:     ((alist-maximize (key acc alist)
50:        `(setf (alist-get ,key ,acc)
51:               (max (alist-get ,key ,acc) (alist-get ,key ,alist 0)))))
52:   (cl-loop for line in aoc-games-played-input
53:            for game = (line->game line)
54:            sum (cl-loop with maxs = (copy-tree maxes)
55:                         for x in (cdr game)
56:                         do (progn
57:                              (alist-maximize 'red maxs x)  
58:                              (alist-maximize 'green maxs x)
59:                              (alist-maximize 'blue maxs x) )
60:                         finally return (apply #'* (map-values maxs)))))
72227

The result happens to be correct for my dataset, here too.

The main highlight here is the cl-macrolet form, which defines local macros, valid only in the lexical scope of the form. It's great for getting syntactic clutter out of the way without polluting the global namespace.

A honorary mention goes to setf - a versatile tool for updating contents of things. There are many "things" that you can set/update, and you can your own "places" and have setf work with them, too. The selling point of setf is the symmetry: most often, the "place" has the same syntactic form as the getter for a given thing. Here, alist-get is used with setf to update a value in an alist - it's way cleaner than assoc and setcdr to which it expands to.

Notice also the copy-tree - it's important because of the mutability of Elisp's lists. It's obvious now when the alist is defined outside of the function, but the call to copy-tree would be needed even if the variable was inlined! That is, this:

(cl-loop with maxs = '((red . 0) (green . 0) (blue . 0)) ...)

Would still produce wrong output. An alternative to copy-tree would be constructing the list at runtime by calling list and cons functions. I'll leave the detail as to why this happens for a future post.

Advent of Code - Feeding hungry Elves, with Elisp ¶

Surprisingly fun exercise!

Created: · Updated:

Solutions to the previous (2022) year AoC first puzzle.

Input looks like this:

123
324

2

4322
3

IOW, a list of lists of numbers. We need to sum the sublists and find the biggest sum.

First solution uses Calc: it grabs one sublist and puts it on the stack as a vector. It then reduces the vector by applying + function, leaving only the sum on the stack. This repeats in a loop until we have a stack of sums. Finally, we pack the whole stack into a single vector and select the maximum element from it. That's it!

1: (save-excursion
2:   (calc-pop (calc-stack-size))
3:   (while (progn (skip-chars-forward " \t\n") (not (eobp)))
4:     (save-selected-window
5:       (calc-grab-region (point) (progn (forward-paragraph) (point)) nil)
6:       (calc-eval "VR+" 'macro)))
7:   (calc-pack (calc-stack-size))
8:   (calc-vector-max nil)
9:   (calc-top-n 1))

However, it's INCREDIBLY slow 🙂 There are many reasons for that, I think the major one is that we're scripting an actual Calc instance and we need to wait for it to update its display after every command.

The second solution is instantaneous - it reads the whole buffer at once, splits the lines, converts textual representation to actual numbers, sums sublists, sorts them and returns the biggest element.

1: (cl-labels
2:     ((split-to-numbers (lst)
3:        (--map (->> (s-split "\n" it) (-map #'string-to-number)) lst)))
4:   (skip-chars-forward " \t\n")
5:   (let* ((input (buffer-substring-no-properties (point) (point-max)))
6:          (subs (->> input s-trim (s-split "\n\n") split-to-numbers))
7:          (sorted (cl-sort (-map #'-sum subs) #'>)))
8:     (message "Winner: %s" (car sorted))))

That's it for the last year. Let's see if I can play a little with the 2023 edition! 🙂

Make <TAB> select the other Dired window ¶

Replicating the best feature of Sunrise Commander in plain Dired

Created:

Some time ago, Sunrise Commander - a two-pane file manager for Emacs, built on top of Dired by Drew Adams - stopped working for me. The loss of familiar bindings was a little painful, but I mostly solved it with Hydra - the functionality is mostly still there in Dired.

One thing I missed was swithcing to the other Dired window easily. With just two Dired windows occupying a whole frame just M-x other-window worked well, but with more windows, I had to fall back to M-x windmove-* and that was less convenient1.

I decided to fix it at some point, and here's the result:

 1: (defun my-select-other-dired-window ()
 2:   "Select the other dired window, if there is only one."
 3:   (interactive)
 4:   (let* ((selected-window (selected-window))
 5:          (other-windows (cdr (window-list))))
 6:     (cl-loop for w in other-windows
 7:              if (with-current-buffer (window-buffer w)
 8:                   (derived-mode-p 'dired-mode)) 
 9:              collect w into result
10:              finally
11:              (when (= 1 (length result))
12:                (select-window (car result))))))
13: 
14: (keymap-set dired-mode-map "<tab>" #'my-select-other-dired-window)

Now I can select the other Dired buffer by pressing <TAB> in one of them, no matter how many other windows are there.

Footnotes:

1

Testing footnotes

How badly can a simple Kotlin function be written? ¶

An abomination lurking deep in a production codebase...

Created:

This is a real gem, found in the middle of a fairly large codebase for more than two years:

 1: object SomeFileUtils {
 2:     fun getFilePath(context: Context, fileName: String, @RawRes resId: Int): String =
 3:         File(context.filesDir, fileName)
 4:             .let { file ->
 5:                 if (file.canRead() && file.length() > 0) {
 6:                     file.absolutePath
 7:                 } else {
 8:                     context
 9:                         .resources
10:                         .openRawResource(resId)
11:                         .use { inputStream ->
12:                             file
13:                                 .outputStream()
14:                                 .use { outputStream ->
15:                                     inputStream.copyTo(outputStream)
16:                                 }
17:                             file.absolutePath
18:                         }
19:                 }
20:             }
21: }

It's so wrong that it's almost beautiful… Almost.

The person who originally wrote this code must have been a little strange in the head, which isn't that unusual in this industry. The reviewer of this code who agreed to merge it - if they even existed - should probably reflect on how serious they are about doing their job, but well - it happens to the best of us. A single fuckup like this is nothing too unusual.

The real tragedy starts after that, though. Code is read much more often than it is written, so it had to be read by other programmers in the time it existed. Especially since many different programmers worked on a project, most of them only for a short time. Now, not one of these people took it upon themselves to eradicate this monstrosity, even though it would be 5 minutes of work. Here's how could it have looked like:

 1: import com.companyname.utils.using
 2: fun createFileForResource(context: Context, fileName: String, @RawRes resId: Int): File {
 3:     val file = File(context.filesDir, fileName)
 4:     if (file.canRead().not() || file.length() == 0L) {
 5:         val resource = context.resources.openRawResource(resId)
 6:         using(resource, file.outputStream()) { input, output ->
 7:             input.copyTo(output)
 8:         }
 9:     }
10:     return file
11: }

I'll start by appealing to authority (because I'm just as lazy as those that will, no doubt, persecute me with Uncle Bob quotes):

Sometimes, the elegant implementation is just a function. Not a method. Not a class. Not a framework. Just a function. – John Carmack

I happen to agree, and it seemed like it was the case in this particular instance, so that's the first change to the code.

Switching to the block body was obvious, since that's the only way to declare vals in direct function scope, and it looked like giving a name to a subexpression could be helpful.

Flipping the condition not only highlighted the exact conditions (explicitly) in which the function does something special, but also allowed the use of just an if statement and dropping the use of else block. It's now clear that, no matter what happens, this function will return a File object - there's only one return statement, and it's explicit, making it really hard to miss.

Simultaneously using more than one Closeable instance is a pretty common thing to do, and deserves its own helper function. You can find using implementation on Github - it's boring, schematic code, so writing it yourself wouldn't be too fun. It's a single file liberally licensed utility that you can just copy and paste to your utils module.

Finally, since stringly-typed code is an abomination that should be eradicated from the face of the Earth, we return the File object itself, rather then just the path to it. The calling code knows better in what form it wants the file to be: if it's for printing, file path is OK, but if you wanted to read it, you'd have to create a new File object based on the returned path. It's always better to leave the representation decisions to the caller, and it pays off to return the most universal representation you have access to.