Paul Graham의 《해커와 화가》를 보면 본인이 생각하는 이상적인 Lisp을 만드는 내용이 나오는데, 그게 바로 Arc. Hacker News가 초기에 Arc로 작성되어 있었다는 것은 잘 알고 있었는데, 여태까지도 Arc로 작성된 채로 유지되고 있을 줄은 몰랐다. 이제서야 Common Lisp으로 바꾼 게 놀라울 정도.
Paul Graham의 《해커와 화가》를 보면 본인이 생각하는 이상적인 Lisp을 만드는 내용이 나오는데, 그게 바로 Arc. Hacker News가 초기에 Arc로 작성되어 있었다는 것은 잘 알고 있었는데, 여태까지도 Arc로 작성된 채로 유지되고 있을 줄은 몰랐다. 이제서야 Common Lisp으로 바꾼 게 놀라울 정도.
한국 페디버스 개발자 모임에서 주최한 스프린트에 다녀왔다. 기여하고자 했던 https://github.com/fedify-dev/fedify/issues/169 는 또 JSON-ish 관련 이슈로 해결하지 못했지만 소규모 모임에서 얻을 수 있는 좋은 분위기와 에너지를 맘껏 느껴 좋았다. PR은 보내지 못했지만 Fedify 마스코트(이름 없음)로 키링 만들어 나눠드려 그나마 다행이었다. 행사 준비하고 진행해준 @hongminhee 님께 감사드린다.
페디버스 행사가 있는 날에 하필 트위터 서비스가 고장나서 오랜만에 마스토돈에 글 남겨본다 ㅋㅋ
Had a wonderful time today at our second FediDev KR #sprint (@sprints.fedidev.kr) gathering at Turing's Apple (@TuringAppleDev) in #Seoul!
We spent the day contributing to various #fediverse open source projects including @fedify, @hollo, and Hackers' Pub. It was fantastic to see the community come together to build and improve tools for the decentralized social web.
Our participants made some great contributions, and you can read all about what we accomplished in today's blog post.
Looking forward to our next sprint!
Hackers' Pub에 드디어 인용 기능이 구현되었습니다. 인용할 글의 링크를 복사한 뒤 단문 작성창에 붙여넣으시면 해당 글을 인용할지 묻는 창이 뜹니다. 확인을 선택하시면 해당 글이 인용되게 됩니다.
참고로 인용할 글은 꼭 Hackers' Pub의 글이 아니어도 ActivityPub을 지원하는 사이트의 아무 글이나 다 가능합니다. 예를 들어 Mastodon 인스턴스에서 글 링크를 복사해서 붙여도 동작합니다.
내가 쓴 글에 누가 어떻게 인용을 했나 궁금하실 경우, 글 아래에 있는 공유 아이콘 오른쪽에 위치한 반응 아이콘을 누르시면 확인할 수 있습니다. (원래는 공유한 사람 탭만 있었는데 인용 탭이 새로 생겼습니다.)
기술적으로는 FEP-e232 오브젝트 링크 스펙과 Misskey의 인용 확장 스펙, Pleroma의 인용 확장 스펙, 그리고 Fedibird의 인용 확장 스펙을 모두 구현하기 때문에, 인용 기능을 지원하는 현존하는 모든 ActivityPub 서비스와 호환됩니다.
Hackers' Pub에 행동 강령이 있다는 사실, 아셨나요?
우리 커뮤니티는 단순한 기술 토론을 넘어 모든 구성원이 진정으로 환영받는 포용적인 공간을 만들기 위해 상세한 행동 강령을 마련했습니다.
특히 주목할 만한 점은:
구조적 차별에 대한 명확한 입장: “모든 사람을 동등하게 대우한다”는 명목 하에 현실의 구조적 불평등을 무시하지 않으며, 이를 극복하기 위한 적극적인 노력을 중요시합니다.
기술적 엘리트주의 지양: “이것도 모르세요?”와 같은 조롱, 특정 기술 스택이나 도구에 대한 비하, 초보자의 질문을 무시하는 행위를 명확히 금지합니다.
모든 언어의 동등한 존중: 전 세계의 모든 언어를 동등하게 존중하며, 어떤 언어로도 자유롭게 소통할 수 있습니다.
자세한 내용은 행동 강령 페이지에서 확인하실 수 있습니다.
Revisiting Java's Checked Exceptions: An Underappreciated Type Safety Feature
https://hackers.pub/@hongminhee/2025/revisiting-java-checked-exceptions
Despite their bad reputation in the Java community, checked exceptions provide superior type safety comparable to Rust's Result<T, E> or Haskell's Either—we've been dismissing one of Java's best features all along.Introduction Few features in Java have been as consistently criticized as checked exceptions. Modern Java libraries and frameworks often go to great lengths to avoid them. Newer JVM languages like Kotlin have abandoned them entirely. Many experienced Java developers consider them a design mistake. But what if this conventional wisdom is wrong? What if checked exceptions represent one of Java's most forward-thinking features? In this post, I'll argue that Java's checked exceptions were ahead of their time, offering many of the same type safety benefits that are now celebrated in languages like Rust and Haskell. Rather than abandoning this feature, we should consider how to improve it to work better with modern Java's features.Understanding Java's Exception Handling Model To set the stage, let's review how Java's exception system works:<Unchecked exceptions (subclasses of RuntimeException or Error): These don't need to be declared or caught. They typically represent programming errors (NullPointerException, IndexOutOfBoundsException) or unrecoverable conditions (OutOfMemoryError).<Checked exceptions (subclasses of Exception but not RuntimeException): These must either be caught with try/catch blocks or declared in the method signature with throws. They represent recoverable conditions that are outside the normal flow of execution (IOException, SQLException).<Here's how this works in practice:// Checked exception - compiler forces you to handle or declare itpublic void readFile(String path) throws IOException { Files.readAllLines(Path.of(path));}// Unchecked exception - no compiler enforcementpublic void processArray(int[] array) { int value = array[array.length + 1]; // May throw ArrayIndexOutOfBoundsException}<The Type Safety Argument for Checked Exceptions At their core, checked exceptions are a way of encoding potential failure modes into the type system via method signatures. This makes certain failure cases part of the API contract, forcing client code to explicitly handle these cases. Consider this method signature:public byte[] readFileContents(String filePath) throws IOException<The throws IOException clause tells us something critical: this method might fail in ways related to IO operations. The compiler ensures you can't simply ignore this fact. You must either:Handle the exception with a try-catch blockPropagate it by declaring it in your own method signature<This type-level representation of potential failures aligns perfectly with principles of modern type-safe programming.Automatic Propagation: A Hidden Advantage One often overlooked advantage of Java's checked exceptions is their automatic propagation. Once you declare a method as throws IOException, any exception that occurs is automatically propagated to the caller without additional syntax. Compare this with Rust, where you must use the ? operator every time you call a function that returns a Result:// Rust requires explicit propagation with ? for each callfn read_and_process(path: &str) -> Result<(), std::io::Error> { let content = std::fs::read_to_string(path)?; process_content(&content)?; Ok(())}// Java automatically propagates exceptions once declaredvoid readAndProcess(String path) throws IOException { String content = Files.readString(Path.of(path)); processContent(content); // If this throws IOException, it's automatically propagated}<In complex methods with many potential failure points, Java's approach leads to cleaner code by eliminating the need for repetitive error propagation markers.Modern Parallels: Result Types in Rust and Haskell The approach of encoding failure possibilities in the type system has been adopted by many modern languages, most notably Rust with its Result<T, E> type and Haskell with its Either a b type. In Rust:fn read_file_contents(file_path: &str) -> Result<Vec<u8>, std::io::Error> { std::fs::read(file_path)}<When calling this function, you can't just ignore the potential for errors—you need to handle both the success case and the error case, often using the ? operator or pattern matching. In Haskell:readFileContents :: FilePath -> IO (Either IOException ByteString)readFileContents path = try $ BS.readFile path<Again, the caller must explicitly deal with both possible outcomes. This is fundamentally the same insight that motivated Java's checked exceptions: make failure handling explicit in the type system.Valid Criticisms of Checked Exceptions If checked exceptions are conceptually similar to these widely-praised error handling mechanisms, why have they fallen out of favor? There are several legitimate criticisms:1. Excessive Boilerplate in the Call Chain The most common complaint is the boilerplate required when propagating exceptions up the call stack:void methodA() throws IOException { methodB();}void methodB() throws IOException { methodC();}void methodC() throws IOException { // Actual code that might throw IOException}<Every method in the chain must declare the same exception, creating repetitive code. While automatic propagation works well within a method, the explicit declaration in method signatures creates overhead.2. Poor Integration with Functional Programming Java 8 introduced lambdas and streams, but checked exceptions don't play well with them:// Won't compile because map doesn't expect functions that throw checked exceptionsList<String> fileContents = filePaths.stream() .map(path -> Files.readString(Path.of(path))) // Throws IOException .collect(Collectors.toList());<This forces developers to use awkward workarounds:List<String> fileContents = filePaths.stream() .map(path -> { try { return Files.readString(Path.of(path)); } catch (IOException e) { throw new UncheckedIOException(e); // Wrap in an unchecked exception } }) .collect(Collectors.toList());<3. Interface Evolution Problems Adding a checked exception to an existing method breaks all implementing classes and calling code. This makes evolving interfaces over time difficult, especially for widely-used libraries and frameworks.4. Catch-and-Ignore Anti-Pattern The strictness of checked exceptions can lead to the worst possible outcome—developers simply catching and ignoring exceptions to make the compiler happy:try { // Code that might throw} catch (Exception e) { // Do nothing or just log}<This is worse than having no exception checking at all because it provides a false sense of security.Improving Checked Exceptions Without Abandoning Them Rather than abandoning checked exceptions entirely, Java could enhance the existing system to address these legitimate concerns. Here are some potential improvements that preserve the type safety benefits while addressing the practical problems:1. Allow lambdas to declare checked exceptions One of the biggest pain points with checked exceptions today is their incompatibility with functional interfaces. Consider how much cleaner this would be:// Current approach - forced to handle or wrap exceptions inlineList<String> contents = filePaths.stream() .map(path -> { try { return Files.readString(Path.of(path)); } catch (IOException e) { throw new RuntimeException(e); } }) .collect(Collectors.toList());// Potential future approach - lambdas can declare exceptionsList<String> contents = filePaths.stream() .map((String path) throws IOException -> Files.readString(Path.of(path))) .collect(Collectors.toList());<This would require updating functional interfaces to support exception declarations:@FunctionalInterfacepublic interface Function<T, R, E extends Exception> { R apply(T t) throws E;}<2. Generic exception types in throws clauses Another powerful enhancement would be allowing generic type parameters in throws clauses:public <E extends Exception> void processWithException(Supplier<Void, E> supplier) throws E { supplier.get();}<This would enable much more flexible composition of methods that work with different exception types, bringing some of the flexibility of Rust's Result<T, E> to Java's existing exception system.3. Better support for exception handling in functional contexts Unlike Rust which requires the ? operator for error propagation, Java already automatically propagates checked exceptions when declared in the method signature. What Java needs instead is better support for checked exceptions in functional contexts:// Current approach for handling exceptions in streamsList<String> contents = filePaths.stream() .map(path -> { try { return Files.readString(Path.of(path)); } catch (IOException e) { throw new RuntimeException(e); // Lose type information } }) .collect(Collectors.toList());// Hypothetical improved APIList<String> contents = filePaths.stream() .mapThrowing(path -> Files.readString(Path.of(path))) // Preserves checked exception .onException(IOException.class, e -> logError(e)) .collect(Collectors.toList());<4. Integration with Optional<T> and Stream<T> APIs The standard library could be enhanced to better support operations that might throw checked exceptions:// Hypothetical APIOptional<String> content = Optional.ofThrowable(() -> Files.readString(Path.of("file.txt")));content.ifPresentOrElse( this::processContent, exception -> log.error("Failed to read file", exception));<Comparison with Other Languages' Approaches It's worth examining how other languages have addressed the error handling problem:Rust's Result<T, E> and ? operator Rust's approach using Result<T, E> and the ? operator shows how propagation can be made concise while keeping the type safety benefits. The ? operator automatically unwraps a successful result or returns the error to the caller, making propagation more elegant. However, Rust's approach requires explicit propagation at each step, which can be more verbose than Java's automatic propagation in certain scenarios.Kotlin's Approach Kotlin made all exceptions unchecked but provides functional constructs like runCatching that bring back some type safety in a more modern way:val result = runCatching { Files.readString(Path.of("file.txt"))}result.fold( onSuccess = { content -> processContent(content) }, onFailure = { exception -> log.error("Failed to read file", exception) })<This approach works well with Kotlin's functional programming paradigm but lacks compile-time enforcement.Scala's Try[T], Either[A, B], and Effect Systems Scala offers Try[T], Either[A, B], and various effect systems that encode errors in the type system while integrating well with functional programming:import scala.util.Tryval fileContent: Try[String] = Try { Source.fromFile("file.txt").mkString}fileContent match { case Success(content) => processContent(content) case Failure(exception) => log.error("Failed to read file", exception)}<This approach preserves type safety while fitting well with Scala's functional paradigm.Conclusion Java's checked exceptions were a pioneering attempt to bring type safety to error handling. While the implementation has shortcomings, the core concept aligns with modern type-safe approaches to error handling in languages like Rust and Haskell. Copying Rust's Result<T, E> might seem like the obvious solution, but it would represent a radical departure from Java's established paradigms. Instead, targeted enhancements to the existing checked exceptions system—like allowing lambdas to declare exceptions and supporting generic exception types—could preserve Java's unique approach while addressing its practical limitations. The beauty of such improvements is that they'd maintain backward compatibility while making checked exceptions work seamlessly with modern Java features like lambdas and streams. They would acknowledge that the core concept of checked exceptions was sound—the problem was in the implementation details and their interaction with newer language features. So rather than abandoning checked exceptions entirely, perhaps we should recognize them as a forward-thinking feature that was implemented before its time. As Java continues to evolve, we have an opportunity to refine this system rather than replace it. In the meantime, next time you're tempted to disparage checked exceptions, remember: they're not just an annoying Java quirk—they're an early attempt at the same type safety paradigm that newer languages now implement with much celebration. What do you think? Could these improvements make checked exceptions viable for modern Java development? Or is it too late to salvage this controversial feature? I'm interested in hearing your thoughts in the comments.
아직 招待制(초대제)이긴 하지만 Hackers' Pub도 velog의 代案(대안)입니다! 게다가 ActivityPub도 支援(지원)하기 때문에 Mastodon이나 Misskey 等(등) 다른 聯合宇宙(연합우주)(fediverse)와도 아무 問題(문제) 없이 相互(상호) 疏通(소통) 可能(가능)하답니다.