transit-lang-cmp - 同じトランジットデータアプリを再実装することによるプログラミング言語の比較

(Programming language comparison by reimplementing the same transit data app)

Created at: 2022-10-02 07:52:04
Language: Elixir
License: MIT

いくつかのプログラミング言語の非公式な比較

このリポジトリは、同じ単純なバックエンド API をさまざまな言語で実装しています。これは言語の感触をつかむための私の個人的なプロジェクトに過ぎず、あまり真剣に受け止められるべきではありません。これまでのところ、私はC#、Typescript(Deno)、Elixir、Go、Rust、Scalaで構築しました。リポジトリにスターを付けたり、プロジェクトの更新が必要な場合はTwitter(@losvedir)でフォローしてください。私は最終的にSwift、Kotlin、普通のJava、Nim、Zigにたどり着きたいと思っています。また、別の言語を提案したい場合は問題を、実装したい場合はPRを自由に開いてください!

すべてのアプリは、交通機関の標準仕様である MBTA の GTFS データ (システムのルート、停留所、スケジュールなど) を読み取ります。アプリはフォルダ内のファイルを探しますが、どの交通システムのデータでも機能するように簡単に更新できます。MBTA データを取得するには、リポジトリのルートディレクトリで次のコマンドを実行します。

MBTA_GTFS

> curl -o MBTA_GTFS.zip https://cdn.mbta.com/MBTA_GTFS.zip
> unzip -d MBTA_GTFS MBTA_GTFS.zip

アプリはすべて、「トランジット」とプログラミング言語名のマッシュアップと名付けられています。

今のところ、アプリはGTFSトリップとstop_timesデータのみを読み込みます。CSV以外のファイルを解析して、メモリ内の構造体のリストを作成します。私はそれがIOの束なので、これがどれくらいの時間がかかるかを見ることに興味がありました - MBTAデータにはおよそ75kの旅行と200万のstop_timesがあります。将来のイテレーションでは、どの旅行がどの日に実行されるかを指定する "サービス"も処理したいと思います。

.txt

アプリは、要求に応答し、そのルートのすべての「スケジュール」(stop_timesを含む旅行)のJSON応答を返す単純なWebサーバーを設定します。これには、トリップによる「結合」が含まれ、一部のルートでは大量のデータをシリアル化します。(私が思うに、最も多いのは、約7MBの応答で赤い線です。これまでは一部の機能しか実装していませんが、route_idやservice_idで旅行を検索し、stop_timesやtrip_idでstop_idを検索することは概念的には理にかなっていると思うので、このように表現しました。したがって、データをハッシュマップとして格納するよりも、データを大きなリストとして格納し、さまざまなハンドルを持つ方が良いと考えました。

/schedules/:route

データ

現在、私は2つのことを集めています。1 つ目は、アプリが GTFS ファイルをメモリ内構造に読み込むのにかかる時間です (より効率的にアクセスするためのハッシュマップ "インデックス" と共に)。2 つ目は、k6 ツールを使用して測定した Web サーバーがフィールドできる 1 秒あたりの要求数です。

stop_times.txt

ローディングstop_times.txt

これは、アプリが約100MBと2Mのレコードであるstop_times.txtファイルをロードし、トリップIDからインデックスのリストへのマップであるトリップの「インデックス」とともに、構造化構造体の大きなベクトル/リスト/配列に解析するのにかかる時間です大きな停止時間リスト。

StopTime

言語 時間 (ミリ秒)
C# 1,390
デノ 2,905
エリキシル 5,986
行く 842
565
スカラ座 931
ティッカー ~ 4,000

ウェブサーバーのパフォーマンス

これは、私は自作経由でインストールしたK6ツールを使用してテストされています。リポジトリのルートにスクリプトがあり、次のようにテストを実行しました。

loadTest.js

k6 run -u 50 --duration 30s loadTest.js

これにより、50人の「仮想ユーザー」がサーバーと同時にアクセスし、テスト自体では、約100のルートのスケジュールのリクエストを順番に発行します。

私は完全に均一なテスト環境を試みているわけではありませんが、通常のアプリのほとんどを閉じて、ラップトップ上で単独で実行しています。あなたは本当に同じマシン上でテストするべきではありませんが、テストハーネスからの負荷が応答データをあまりにもひどく混乱させないと思ったので、要求は全体的にかなり強烈です。ここでは、k6が吐き出す要求/秒と、好奇心からActivityMonitorで見た最高のRAMとCPU使用率の目玉を報告します。

JSONヘビー

これらは、約100のMBTAルートを含むファイルを使用し、その多くはメガバイト単位のJSONスケジュールデータを持っています。したがって、ここでのパフォーマンスは、主にJSONをシリアライズできる速度を反映しています。これらはすべて、50人の同時仮想ユーザーで行われました。

loadTest.js

言語 要求数/秒 最大 CPU (%) 最大メモリ (メガバイト)
C# 1,534 654 1,750
デノ 286 280 400
エリキシル 388 750 3,700
行く 2,715 620 1,100
2,839 619 603
スカラ座 432 715 3,150

小さい回答

これらはランナーを使用し、スケジュールデータが〜50KBから〜200KBの範囲にある約12のルートしか使用しないため、要求ははるかに高く、JSONエンコーディングによって支配されることは少なくなります。応答が小さく、より多くの要求を処理できるため、同時「仮想ユーザー」の数も異なる数で試しました。

loadTestSmallResponses.js

1 秒あたりの要求数、言語別、同時仮想ユーザー数 (高いほど良い)。

言語 1 VU 10 VU 50 VU 100 VU
C# 2,227 11,663 13,005 13,102
デノ 2,808 3,882 3,852 3,753
エリキシル 623 3,306 3,809 3,852
行く 2,283 10,551 11,078 11,091
3,501 20,496 22,437 22,283
スカラ座 705 4,204 4,289 4,332

ミリ秒単位の応答時間: 中央値 / p95 / max、言語および同時仮想ユーザー数別 (低いほど良い):

言語 1 VU 10 VU 50 VU 100 VU
C# .3 / 1 / 88 .6 / 2 / 27 2 / 12 / 138 6 / 17 / 75
デノ .3 / .8 / 5 2 / 4 / 254 13 / 16 / 218 26 / 33 / 265
エリキシル 1 / 4 / 11 2 / 7 / 22 13 / 23 / 58 24 / 47 / 132
行く .3 / 1 / 19 .6 / 2 / 36 3 / 15 / 111 6 / 29 / 140
.2 / .6 / 3 .4 / 1 / 9 2 / 4 / 36 4 / 9 / 85
スカラ座 1 / 3 / 109 2 / 5 / 129 3 / 58 / 394 10 / 109 / 587

データの検索

このメトリックは、以前のコミットから収集し、単にレッドラインのStopTimesの数をカウントする必要がありました。私はWebサーバーのアプローチを支持してこのコードを削除しましたが、後世のためにここに統計を保持しています。

言語 時間 (ミリ秒)
C# 1.0
デノ 1.4
エリキシル 3.2
行く 0.4
0.7
スカラ座 2.5
ティッカー 13

感想

私がこれを書いている間に散らばった考えがいくつかあります。

C#

どこから始めましょうか?まず、マイクロソフトのエコシステムのすべての異なる部分が何であるかという専門用語レベルで、この非常に混乱しました。C#は言語であり、 ".NET CLR"上で実行されます。ビルド/実行ツールは、それがメイン用語のようなものですが、私はまた、 "CLR"が投げられているのを見ました。私はすべてのガイドとドキュメントがそれと呼んでいたものであり、クロスプラットフォームであった ".NET 6.0"で作業することになりました。私は期待していたようにどこにも ".NET Core"を見ませんでしたが、これは明示的にクロスプラットフォームの作品だったと思いますか?面白いことに、私は標準の ".NET Web Framework"を調べようとかなりの時間を費やしましたが、最終的にそれが ASP.NET れが何であるかに最終的に気付きました。私は "ASP"をたくさん見てきましたが、それが写真にどのように収まるのか分からなかったので、それは私のために接続するのに便利でした。

dotnet

私はこのプロジェクトを完了できると完全には確信していませんでした。実際には、クロスプラットフォームの.NETがどれほど本当にあるのか分かりませんでしたが、開発は滞りなく進みました!私は、はい、少なくとも標準ライブラリと ASP.NET を使用する私の単純なユースケースでは、それは本当にクロスプラットフォームです。私はサードパーティのライブラリを持ち込もうとはしませんでした、そして私はそこにいくつかの非互換性があるかもしれないと思います。将来は、自分の感性とよりインラインな言語であるF#を探求していきたいと思いますが、まずはもっと「バニラ.NET」を試してみたかったのです。VSCodeの開発者の経験は素晴らしく、言語サーバーはうまく機能し、コードフォーマッタは機能しました(ただし、次の行で中括弧を開くという慣習は軽蔑しています)。

言語に関しては、C#は...大丈夫、私は推測する。それは私にダートを思い出させるようなものです。それは正常に動作し、ツーリングは良いです、それは冗長で非常にオブジェクト指向ですが、それは本当に喜びを呼び起こしません。「10億ドルの間違い」は私にとって重要であり、C#には型システムにnull不可能な砂糖がありますが(つまり、いくつかの型の後)、型システムは私が望んでいたほど厳密ではありませんでした。ある時点で、私は代わりに偶然にやったので、私はバグがありました。前者は like ではなく構造体なので、直感的には分割できないように感じますが、最善の努力をして何かをしましたが、私は何がわからないのですか?

?
stopWatch.Elapsed / 1000
stopWatch.ElapsedTicks / 1000
TimeSpan
long
ElapsedTicks

ASP.NET には多くの慣習と魔法があります。私は個人的にその魔法をすべて愛しているわけではありませんが、あなたがそれを経験しているなら、私はそれがどのようにWebアプリケーションを設計するかをかなり速くする方法を見ることができました。

しかし、うわー、私は信じられないほど驚き、パフォーマンスに感銘を受けました!それは私の最適化されていないRustに匹敵しました(つまり、Rustをクローンがたくさんある高水準言語のように扱う)!

全体として、私はうれしいことに驚き、ドットネットとC#にかなり感銘を受けました。

デノ

デノはかなりきれいです。私は本当にそれが成功することを望みます。私は本当にTypeScriptが好きで、Denoは私が欲しいものをほとんど与えてくれます:TypeScriptは、フロントエンド以外のアプリケーションを構築できる標準ライブラリを備えた本格的な言語です。JSの遺産とV8のものを敷物の下にすべて掃きましょう...

私は標準ライブラリが他のすべてのパッケージのようなURLになかったらいいのにと思います。ダウンロードしたツールに標準ライブラリも含まれていて、ネットワークのものなしで参照できれば素晴らしいでしょう。ドキュメントもかなり謎めいています(そして私は自動生成されたと思いますか?

deno

パッケージ管理のものは、私が頭を包んでいません。明らかに、あなたは喜んでものをダウンロードするべきではありませんが、私は従来のdeps.ts、import_map、ロックファイルの指定、ベンダーコマンド、 --no-remoteなどのいくつかの組み合わせで、私は合理的なアプローチを構築するためのすべての部分を持っているように感じますが、私はまだそれをすべて理解していません。

個人的には、 、などのものは私には少しギミックを感じます。私は他の言語が本当にそれを持っているとは思わないし、脅威モデルがここにあるのかはわからない。私はバックエンドコードを制御しますが、自分のコードがそのような予期しないことをすることを心配している場合は、より大きな問題があります。私はいつも一緒に走っています。

--allow-read
--allow-net
-A

単一の仮想ユーザーを見るとパフォーマンスは素晴らしかったですが、そこには一種のトップがありました。非同期と複数のコアをうまく処理できないのか、それとも何か間違ったことをしていたのかはわかりません。

エリキシル

エリクサーは私の第一言語なので、私は私が欠けているかもしれないものを見るためにそのパフォーマンスを比較するためにこれを投げました。私はElixirの言語とそのすべての素晴らしいOTPグッズが好きですが、それは少し遅いことが知られているので、私はテーブルにどれだけのパフォーマンスを残しているのか疑問に思っていました。

通常、Elixirのいくつかの状態について最初に考えたのはエージェントまたはカスタムGenServerですが、それはすべての要求を1つのデータソースに注ぎ込み、順番に応答し、負荷がかかっているとボトルネックになる可能性があると考えました。そこで、読み取り同時実行性を有効にして、ETSにデータを入れることにしました。

ETSはデータをErlangタプルのセット(この場合)として格納し、他のアプリケーションの規則に従いたいと考え、「インデックス」の目的で、各タプルに余分な「主キー」整数を追加することにしました。他の言語では、基になるリストにインデックスを付けるだけで済みますが、ETS がデータを格納する方法では実際には不可能です。

このアプローチは、アプリの起動時にロードされるGTFS静的データではうまく機能しますが、リアルタイムの車両位置と予測データをポーリングするようにアプリケーションを拡張した場合、データを更新する必要があるときにどのように処理するかはまだ完全にはわかりません。このシナリオでは、ETS データのロックとアトミック更新の処理方法に以前から問題がありました。ほとんどの場合、バックグラウンドでまったく新しいETSテーブルを作成し、準備ができたらこのテーブルと交換するようなものです。

私はここでフェニックスを使用しました、なぜならそれはElixirの標準とほぼ同じだからです、しかしそれは他のアプリより少し重い重量かもしれません。しかし、私の理解では、コンパイルされるのはほとんどがプラグだけなので、実際にパフォーマンスにどれだけ影響するかは、私ができる最小限のことに対してかなり軽量です。

最終的なパフォーマンス結果は残念ながら低く、Rustよりも桁違いに悪く、Denoより少し遅れていました。

行く

私は標準ライブラリだけを使ってこれまでの作業を成し遂げることができてとてもうれしかったです。そしてパフォーマンスはしっかりしていました!JSONを多用するベンチマークでは、実際にはすべての言語の中で最も高速ですが、応答の軽いベンチマークでは、私が期待していた場所は高速ですが、錆のレベルではありません。

とは言っても、私の期待に反して、私はドキュメントが素晴らしくないことを発見しました。言語リファレンスとツアーはかなり良くて便利でしたが(私はツアーを参照し続けました)、pkg.go.dev のライブラリドキュメントは公正でした...悪い。

CSV解析パッケージが取るものであるdang 、 を取得する方法を理解するのに私が認めたいよりも時間がかかりました。私は彼らのドキュメントを検索すると、(1)ファイルシステムから読み込み、(2)インターフェイスを実装するパッケージや関数が一目で得られることを望んでいましたが、一番の結果は単にインターフェイスの定義であり、残りの結果はランダムなGitHubリポジトリでした。また、定義をクリックしても、定義を実装するものへのリンクは提供されませんでした。結局、私はあきらめて、ファイルを開いて読む方法を見つけようと、反対の方向に行きました。私はついに見つけました(それはで突っ込んだ後、それは私の3回目の試みでしたが)。私はそれが を返すのを見ました 、そしてそれはそれからそれがドキュメントで言及されていないが、型が実装しているのでそれはすでにあることに気づく前にそれをaに変える方法について少しのガチョウの変更で私を送りました!それは私にとってすべて魔法のようなもので、ちょっと奇妙でした。今、私は理論的には、それを実装する型を見つけるために "Read"を pkg.go.dev に検索することができ、したがって私を満足させ、私を連れて行くことができましたが、検索機能は熱いゴミのように見えるので、もちろんうまくいきません。

io.Reader
io.Reader
io.Reader
io.Reader
os.Open()
io
io.fs
File
io.Reader
Read
io.Reader
io.Reader
File
os.Open()

そうは言っても、実際にGoでプログラミングするのはかなり素晴らしかったです。VSCodeのサポートはしっかりしており、ビルド/実行サイクルは高速でした!最終結果もかなり速くなりました。それは私が感謝するタイプの豊かさを持っていませんが、私は全体的にそれを気にしませんでした。

私はこれがどのように機能するかについての私の期待だったので、私は "Webフレームワーク"を探し始めましたが、単に標準ライブラリを使用することが出発点として良いという合理的なコンセンサスがあるように見えました。それは素晴らしく、分析麻痺を避け、ベンチマークやHNやredditなどを見直して、使用するフレームワークを決定するのに役立ちました。

これは良い意味で私に衝撃を与えました!私はもっと低レベルの煩わしさを期待していました、そして、単に割り当ててクローン化し、私が読んだすべてのトリックをして、可能な限り多くのパフォーマンスを引き出すことを心配しないように準備していました。結局のところ、私は高レベルのインタプリタ言語やGC言語と比較しており、システムレベルでプログラミングする必要よりも、その型システムのためにRustにもっと興味があります。

私は過去にRustで遊んだ経験があったので、私にとってはまったく新しいものではありませんでしたが、それはしばらく経ったので、私はもっと多くなることを期待していました...さびた。つまり、私の最初のアプローチでは、Stringのクローン作成をたくさん行い、他の言語(ベンチマークに応じてdotnetまたはGo)の最高のものに匹敵するパフォーマンスを得ました。しかし、redditの少しの助けの後、私はいくつかの不要なString割り当てを削除し、(私にとっては)ライフタイムマーカーを使用して恐れていたので、応答構造体は実際のデータに割り当てられた文字列を参照するだけで、パフォーマンスは劇的に飛躍し、最もパフォーマンスの高い言語になりました。

&str

また、Rustが特別なのか、BurntSushiが国宝であり、彼のCSVライブラリが完璧に構築され、文書化されているため、これがどれくらいあるのかはわかりません。

私はまた、私の構造体に不要なフィールド(私はTripsのservice_idsやStopTimesの到着時間と出発時間をまだ使用していません)があるというコンパイラの警告を受け取ったことに感銘を受け、面白がっていました。

Webサーバーの部分については、使用するフレームワークを決定しようとしばらく時間を費やしました。私が最後にRustを見たとき、すべての怒りでしたが、最近はほぼ完全にレーダーから落ちたようです!それはやや心配でした。それはほぼ「デフォルト」として引き継がれたようですが、かなり人気があるという新しいものがある点が異なります。東京の公式プロジェクトに参加していて、東京には持続力があると推測して、私は一緒に行きました。

rocket
actix
axum
axum

仕事に取り掛かるのは少し難しかったです...私は自分のアプリをコンパイルするためにタイプテトリスを少し遊んでいるように感じ、それを完全に理解せずに無意識のうちにドキュメントをコピーしようとしていました。私は構文をあまり理解していないので、私の関数に注釈を付けることは私にとってはまだ黒魔術です。私はまた、私が私の依存関係として置く必要があることに気づく前に、しばらくの間つまずいていました。それはaxumのドキュメントにはありませんでしたが、私は彼らの例でそれを見つけましたが、それが私のものではないときに例をコンパイルすることを可能にしたのはかなり野生の推測でした。私はそこに深く埋めていた潜在的な知識を頼りに、それが生態系が構築されたものであり、それは言語の一部ではなく木箱でしたが、それは当時の錆びた人々による実験のための一時的なものにすぎないと思っていました。

#[...]
main
#[tokio::main]
futures
Cargo.toml
futures
async

I also ran into some issues trying to get my shared state to work. My handler was failing to typecheck and the compiler error was not helpful. The axum docs actually mention this is a problem and that there's an crate that can help, though. Some of this was my lack of understanding exactly how works and how to safely have shared state across async requests. In the end, I appreciate that the flexibility is there; right now I just have an so that all my handlers can read the data I prepare up front, but I could see how I could wrap it in an , for example, to also allow safe updates in the future. In general, I'm not sure how I feel about Axum's magical handler/extractor setup, as I still don't really know how it works.

axum-macros
Arc
Arc
RwLock

Scala

Of all the languages I played with here, Scala is the only one I disliked.

Part of the reason was timing: it seems an ecosystem in flux at the moment. It didn't work with my JDK 19 out of the box, so I had to downgrade to JDK 17 for it. I sort of blindly followed the scala-lang.org site and went through the getting started guide, for Scala 3, and then building the first half of my app in Scala 3, before realizing that Play (the only Scala webframework I'd heard of) and Scalatra (the other web framework mentioned in the Getting Started "ecosystem" section of the guide) don't work on Scala 3 yet. I briefly tried updating my code to Scala 2 but I wasn't super sure what the differences were, and I didn't really want to learn Scala 2 if everything is moving to Scala 3 (eventually) anyway. Beyond that I got mixed messages in whether to use and as a build tool.

sbt
mill

Beyond that, there seems to be a schism in the community between people who love super sophisticated types (think Haskell style Applicative Functors or whatever) and people who want a nicer Java (these days those people might be moving to Kotlin).

In trying to find a Scala 3 compatible web framework, I saw a lot of people saying is the new standard, so I tried that one first. But after generating the skeleton for the app, and trying to add my own routes I gave up. The "router" part is unwieldy and complicated, though I think I was able to cargo-cult a route of my own. Here's the given example on how to match against :

http4s
/hello/:name

def helloWorldRoutes[F[_]: Sync](H: HelloWorld[F]): HttpRoutes[F] =
  val dsl = new Http4sDsl[F]{}
  import dsl._
  HttpRoutes.of[F] {
    case GET -> Root / "hello" / name =>
      for {
        greeting <- H.hello(HelloWorld.Name(name))
        resp <- Ok(greeting)
      } yield resp
  }

But then when it comes to implementing the code that actually returns data there, I got totally flummoxed. Here's the corresponding "HelloWorld" code for the above route:

import cats.Applicative
import cats.implicits._
import io.circe.{Encoder, Json}
import org.http4s.EntityEncoder
import org.http4s.circe._

trait HelloWorld[F[_]]:
  def hello(n: HelloWorld.Name): F[HelloWorld.Greeting]

object HelloWorld:
  def apply[F[_]](using ev: HelloWorld[F]): HelloWorld[F] = ev

  final case class Name(name: String) extends AnyVal
  /**
    * More generally you will want to decouple your edge representations from
    * your internal data structures, however this shows how you can
    * create encoders for your data.
    **/
  final case class Greeting(greeting: String) extends AnyVal
  object Greeting:
    given Encoder[Greeting] = new Encoder[Greeting]:
      final def apply(a: Greeting): Json = Json.obj(
        ("message", Json.fromString(a.greeting)),
      )

    given [F[_]]: EntityEncoder[F, Greeting] =
      jsonEncoderOf[F, Greeting]

  def impl[F[_]: Applicative]: HelloWorld[F] = new HelloWorld[F]:
    def hello(n: HelloWorld.Name): F[HelloWorld.Greeting] =
        Greeting("Hello, " + n.name).pure[F]

That's a lot of both syntax and semantics to grok. is an interface, is a singleton class, is kind of a data record. I don't know what or are. I recognize as related to but that's a whole complicated library/type concern distinct from Scala-the-language. I don't really know why the does a with a nested .

trait
object
case class
given
using
pure
Applicative
def impl
new HelloWorld
def hello

In the end, I moved on to trying the framework Cask, which

aims to bring simplicity, flexibility and ease-of-use to Scala webservers, avoiding cryptic DSLs or complicated asynchrony

And in the end, I got something that worked! So thanks author of Cask. Cask also used rather than . The latter seems more "official", or at least is the tool recommended on scala-lang.org, but oh BOY is it slow! was nicer for me to work with.

mill
sbt
mill

パフォーマンスはそれほど素晴らしくはなく、はるかに多くのメモリを使用しました。これがCaskがパフォーマンスに焦点を当てていないためかどうかはわかりませんが、他に何も動作させることができませんでした...私はエコシステムを扱う前にScala 3を十分に気に入っていましたが、将来的にはScalaが2対3の移行を完了するまで、そして非タイプオタクが勝つ場合にのみScalaを避けるつもりです。

ティッカー

実際にはリンゴとリンゴの比較ではありませんが、私はここでSQLiteのパフォーマンス特性の大きさについて興味がありました。

インポートのために私は走っている間に数えました(はい、だから塩の粒でその時間を取ってください):

stop_times

sqlite> .mode csv
sqlite> .import MBTA_GTFS/stop_times.txt stop_times

そして、レッドラインのスケジュールの数のデータスキャンのために、私はしました:

sqlite> create index stop_times_by_trip on stop_times(trip_id);
sqlite> create index trips_by_route on trips(route_id);
sqlite> .timer on
sqlite> select count(*) from stop_times where trip_id in (select trip_id from trips where route_id = "Red");

利便性に勝ることはありません!すべてをメモリに保持するアプリよりも桁違いに遅いですが、もちろんトレードオフははるかに少ないメモリを使用することです!そして、与えられた読み込みは遅い(-ish)が、私はそれの多くがファイルシステム上で待っていること、そして同時読み取りが十分なスループットを可能にするべきであることを理解しています。

速い

Swift の進行中の作業からのメモ。

  • 多くのGBのXcodeをダウンロードしなければならなかったが、これには多くの問題があった(ログインしなければならなかった)
  • やったし、その後、それはクラッシュした。オンラインでの議論が推奨され、HelloWorldが実行されるようになりました。
    swift init
    swift run
    sudo xcode-select --reset
  • コードフォーマッタがありませんか?
  • xcodeで私のプロジェクトから相対MBTA_GTFSフォルダを読み取る方法を理解できませんでしたが、ディレクトリから実行することはうまくいきました。(私はxcodeが必要としなかったのに持っていましたが。
    swift run
    swift package init