首頁> Java> java教程> 主體

即使是偉大的數學家也會犯錯

WBOY
發布: 2024-08-09 18:38:02
原創
161 人瀏覽過

我們知道數學是一門精確的科學。我們可以對互動式數學學習軟體 GeoGebra 說同樣的話嗎?讓我們用PVS-Studio來分析一下專案源碼吧!

Even great mathematicians make mistakes

介紹

你還記得你在大學是如何學習電腦科學的嗎?所有這些矩陣和向量乘法、多項式方程式、插值、外推......如果我們在真實的項目中而不是在另一個實驗室報告中查看這些可怕的公式會怎麼樣?如果我們在這樣的程式碼庫中挖掘問題怎麼辦?我建議運行 PVS-Studio 並撣掉數學教科書的灰塵。為什麼是教科書?讓我告訴你。

數學挑戰

檢查此類程式原始碼的主要挑戰之一是了解正在發生的事情。在查看分析器報告時,我們對警告是否表明真正的問題有疑問。

讓我們來看看下面的片段:

雷雷

我們收到以下 PVS-Studio 警告:

V6072 發現兩個相似的程式碼片段。也許,這是一個拼字錯誤,應該使用“b”變數而不是“a”。 AlgoTriangleDF.java 145、AlgoTriangleDF.java 146、AlgoTriangleDF.java 147、AlgoTriangleDF.java 148

真的是打字錯誤嗎?經過快速研究,一旦我們找到了正確的公式,我們就可以說一切都寫對了。

程式碼片段評估三角分佈,即該分佈的機率密度函數(PDF)。我們找到公式:

Even great mathematicians make mistakes

現在讓我們來看看程式碼。

這裡,* fv* 是一個函數變數。wrap回傳wrapper,然後執行必要的數學運算。有趣的是,有multiplymultiplyR方法。在第二種方法中,R代表並交換運算元,因為乘法並不總是可交換的。

因此,第二個表達式結果寫入branchAToMode,第四個表達式寫入branchModeToB

我們也注意到,在branchModeToB中,分子和分母的符號發生了變化。我們得到以下表達式:

Even great mathematicians make mistakes

表情值沒有變化

因此,我們刷新了數學知識來理解我們收到的一些警告。識別程式碼中是否存在真正的錯誤並不難,但很難理解它應該在這裡。

錯誤

遺失代碼段

我們先從簡單開始,看看下面的方法:

雷雷

我們發現有人忘記將s[2]替換為s[3]。最後一行的效果非常出色。這是傳說中的、非常常見的複製貼上錯誤。結果,第四個數組項丟失並且為null

V6033 具有相同按鍵「2」的項目已被更改。 AlgoPolyhedronNetPrism.java 376、AlgoPolyhedronNetPrism.java 377

價值觀呢?

現在嘗試在以下程式碼片段中發現問題:

雷雷

多麼壯麗的景色啊!讀起來很愉快,這只是一小部分,因為這個方法從第 66 行開始,到第 404 行結束。分析儀發出 50 個 V6033 類型的警告。讓我們快速瀏覽一下其中一個警告:

V6033 已新增相同按鍵 '"∼"' 的項目。 MathMLParser.java 229, MathMLParser.java 355

讓我們刪除多餘的片段,看看警告中提到的表達方式:

雷雷

不過,這很有趣。方法呼叫之間的間隔是多少?有126行。好吧,祝你好運手動發現這樣的錯誤!

大多數是鍵和值重複。然而,有一些情況與上面的範例類似,開發人員用不同的值覆蓋該值。我們應該使用哪一個?

圓形或橢圓形

雷雷

橢圓和圓都會呼叫橢圓的方法。事實上,我們可以假設這是可以的,因為圓也是橢圓。然而,該類別還有updateCircle方法。那應該是什麼?讓我們更深入地探討一下。

一切都發生在DrawConic3D類。以下是橢圓和圓的方法:

protected void updateCircle(PlotterBrush brush) { if (visible == Visible.CENTER_OUTSIDE) { longitude = brush.calcArcLongitudesNeeded(e1, alpha, getView3D().getScale()); brush.arc(m, ev1, ev2, e1, beta - alpha, 2 * alpha, longitude); } else { longitude = brush.calcArcLongitudesNeeded(e1, Math.PI, getView3D().getScale()); brush.circle(m, ev1, ev2, e1, longitude); } } protected void updateEllipse(PlotterBrush brush) { if (visible == Visible.CENTER_OUTSIDE) { brush.arcEllipse(m, ev1, ev2, e1, e2, beta - alpha, 2 * alpha); } else { brush.arcEllipse(m, ev1, ev2, e1, e2, 0, 2 * Math.PI); } }
登入後複製

Well... It doesn't give that much confidence. The method bodies are different, but nothing here indicates that we risk displaying unacceptable geometric objects if the method is called incorrectly.

Could there be other clues? A whole single one! TheupdateCirclemethod is never used in the project. Meanwhile,updateEllipseis used four times: twice in the first fragment and then twice inDrawConicSection3D, the inheritor class of* DrawConic3D*:

@Override protected void updateCircle(PlotterBrush brush) { updateEllipse(brush); } @Override protected void updateEllipse(PlotterBrush brush) { // .... } else { super.updateEllipse(brush); } }
登入後複製

ThisupdateCircleisn't used, either. So,updateEllipseonly has a call in its own override and in the fragment where we first foundupdateForItSelf. In schematic form, the structure looks like as follows:

Even great mathematicians make mistakes

On the one hand, it seems that the developers wanted to use the all-purposeupdateEllipsemethod to draw a circle. On the other hand, it's a bit strange thatDrawConicSection3Dhas theupdateCirclemethod that callsupdateEllipse. However,updateCirclewill never be called.

It's hard to guess what the fixed code may look like if the error is actually in the code. For example, ifupdateCircleneeds to callupdateEllipseinDrawConicSection3D, butDrawConic3Dneeds a more optimized algorithm for the circle, the fixed scheme might look like this:

Even great mathematicians make mistakes

So, it seems that developers once wroteupdateCircleand then lost it, and we may have found its intended "home". Looks like we have discovered the ruins of the refactoring after which the developers forgot about the "homeless" method. In any case, it's worth rewriting this code to make it clearer so that we don't end up with so many questions.

All these questions have arisen because of the PVS-Studio warning. That's the warning, by the way:

V6067 Two or more case-branches perform the same actions. DrawConic3D.java 212, DrawConic3D.java 215

Order of missing object

private void updateOrdering(GeoElement geo, ObjectMovement movement) { .... switch (movement) { .... case FRONT: .... if (index == firstIndex) { if (index != 0) { geo.setOrdering(orderingDepthMidpoint(index)); } else { geo.setOrdering(drawingOrder.get(index - 1).getOrdering() - 1); } } .... } .... }
登入後複製

We get the following warning:

V6025 Index 'index - 1' is out of bounds. LayerManager.java 393

This is curious because, in theelseblock, theindexvariable is guaranteed to get the value 0. So, we pass -1 as an argument to thegetmethod. What's the result? We catch anIndexOutOfBoundsException.

Triangles

@Override protected int getGLType(Type type) { switch (type) { case TRIANGLE_STRIP: return GL.GL_TRIANGLE_STRIP; case TRIANGLE_FAN: return GL.GL_TRIANGLE_STRIP; // <= case TRIANGLES: return GL.GL_TRIANGLES; case LINE_LOOP: return GL.GL_LINE_LOOP; case LINE_STRIP: return GL.GL_LINE_STRIP; } return 0; }
登入後複製

The code is new, but the error is already well-known. It's quite obvious thatGL.GL_TRIANGLE_STRIPshould beGL.GL_TRIANGLE_FANinstead*.* The methods may be similar in some ways, but the results are different. You can read about it under the spoiler.

V6067 Two or more case-branches perform the same actions. RendererImplShadersD.java 243, RendererImplShadersD.java 245

To describe a series of triangles, we need to save the coordinates of the three vertices of each triangle. Thus, given N triangles, we need the saved 3N vertices. If we describe a polyhedral object using a polygon mesh, it's important to know if the triangles are connected. If they are, we can use the Triangle Strip or the Triangle Fan to describe the set of triangles using N + 2 vertices.

We note that the Triangle Fan has been removed in Direct3D 10. In OpenGL 4.6, this primitive still exists.

The Triangle Fan uses one center vertex as common, as well as the last vertex and the new vertex. Look at the following example:

Even great mathematicians make mistakes

To describe it, we'd need the entry (A, B, C, D, E, F, G). There are five triangles and seven vertices in the entry.

The Triangle Strip uses the last two vertices and a new one. For instance, we can create the image below using the same sequence of vertices:

Even great mathematicians make mistakes

Therefore, if we use the wrong primitive, we'll get dramatically different results.

Overwritten values

public static void addToJsObject( JsPropertyMap jsMap, Map map) { for (Entry entry : map.entrySet()) { Object object = entry.getValue(); if (object instanceof Integer) { jsMap.set(entry.getKey(), unbox((Integer) object)); } if (object instanceof String[]) { // <= JsArray clean = JsArray.of(); for (String s: (String[]) object) { clean.push(s); } jsMap.set(entry.getKey(), clean); } else { // <= jsMap.set(entry.getKey(), object); // <= } } }
        
登入後複製

If we don't look closely, we may not notice the issue right away. However, let's speed up and just check the analyzer message.

V6086 Suspicious code formatting. 'else' keyword is probably missing. ScriptManagerW.java 209

Finally, a more interesting bug is here. Theobject instanceof String[]check will occur regardless of the result ofobject instanceof Integer. It may not be a big deal, but there's also theelseblock that will be executed after a failed check forString[]. As the result, thejsMapvalue byentry.getKey()will be overwritten if theobjectwasInteger.

There's another similar case, but the potential consequences are a little harder to understand:

public void bkspCharacter(EditorState editorState) { int currentOffset = editorState.getCurrentOffsetOrSelection(); if (currentOffset > 0) { MathComponent prev = editorState.getCurrentField() .getArgument(currentOffset - 1); if (prev instanceof MathArray) { MathArray parent = (MathArray) prev; extendBrackets(parent, editorState); } if (prev instanceof MathFunction) { // <= bkspLastFunctionArg((MathFunction) prev, editorState); } else { // <= deleteSingleArg(editorState); } } else { RemoveContainer.withBackspace(editorState); } }
登入後複製

V6086 Suspicious code formatting. 'else' keyword is probably missing. InputController.java 854

Almost correct

Do you often have to write many repetitious checks? Are these checks prone to typos? Let's look at the following method:

public boolean contains(BoundingBox other) { return !(isNull() || other.isNull()) && other.minx >= minx && other.maxy <= maxx && other.miny >= miny && other.maxy <= maxy; }
登入後複製

V6045 Possible misprint in expression 'other.maxy <= maxx'. BoundingBox.java 139

We find a typo, we fix it. The answer is simple, there should beother.maxx <= maxx.

Mixed up numbers

public PointDt[] calcVoronoiCell(TriangleDt triangle, PointDt p) { .... // find the neighbor triangle if (!halfplane.next_12().isHalfplane()) { neighbor = halfplane.next_12(); } else if (!halfplane.next_23().isHalfplane()) { neighbor = halfplane.next_23(); } else if (!halfplane.next_23().isHalfplane()) { // <= neighbor = halfplane.next_31(); } else { Log.error("problem in Delaunay_Triangulation"); // TODO fix added by GeoGebra // should we do something else? return null; } .... }
登入後複製

Let's see the warning:

V6003 The use of 'if (A) {...} else if (A) {...}' pattern was detected. There is a probability of logical error presence. DelaunayTriangulation.java 678, DelaunayTriangulation.java 680

We don't even need to figure out what happens to half-planes, because it's clear that the!halfplane.next_31().isHalfplane()check is missing.

Wrong operation

public static ExpressionNode get( ExpressionValue left, ExpressionValue right, Operation operation, FunctionVariable fv, Kernel kernel0 ) { .... switch (operation) { .... case VEC_FUNCTION: break; case ZETA: break; case DIRAC: return new ExpressionNode(kernel0, left, Operation.DIRAC, fv); case HEAVISIDE: return new ExpressionNode(kernel0, left, Operation.DIRAC, fv); default: break; } .... }
登入後複製

It seems that, in the second case,Operation.DIRACshould probably be replaced withOperation.HEAVISIDE. Or not? This method is called to obtain the derivative. After researching, we understand whatHEAVISIDE *is—the use of *Operation.DIRACfor it is correct. What about theDiracderivative? This one is a bit tricky to understand. We may trust the developers and suppress the warning. Still, it'd be better if they'd left any explanatory comment as they had done in some cases before.

case FACTORIAL: // x! -> psi(x+1) * x! return new ExpressionNode(kernel0, left.wrap().plus(1), Operation.PSI, null) .multiply(new ExpressionNode(kernel0, left, Operation.FACTORIAL, null)) .multiply(left.derivative(fv, kernel0)); case GAMMA: // gamma(x) -> gamma(x) psi(x) return new ExpressionNode(kernel0, left, Operation.PSI, null) .multiply(new ExpressionNode(kernel0, left, Operation.GAMMA, null)) .multiply(left.derivative(fv, kernel0));
登入後複製

And we were motivated to conduct such research by the message of already favorite diagnostic V6067:

V6067 Two or more case-branches perform the same actions. Derivative.java 442, Derivative.java 444

On the one hand, this is an interesting case because we could have a false positive here. On the other, the warning would be useful either way, as it often highlights a genuine error. Regardless of the results, we need to check such code and either fix the error or write explanatory comments. Then the warning can be suppressed.

Vector or point?

private static GeoElement doGetTemplate(Construction cons, GeoClass listElement) { switch (listElement) { case POINT: return new GeoPoint(cons); case POINT3D: return (GeoElement) cons.getKernel().getGeoFactory().newPoint(3, cons); case VECTOR: return new GeoVector(cons); case VECTOR3D: return (GeoElement) cons.getKernel().getGeoFactory().newPoint(3, cons); } return new GeoPoint(cons); }
登入後複製

The warning isn't hard to guess:

V6067 Two or more case-branches perform the same actions. PointNDFold.java 38, PointNDFold.java 43

Instead ofcons.getKernel().getGeoFactory().newPoint(3, cons)in the second case,cons.getKernel().getGeoFactory().newVector(3, cons)may have been intended. If we want to make sure of it, we need to go deeper again. Well, let's dive into it.

So,* getGeoFactory()returnsGeoFactory,let's look at the *newPoint *andnewVector* methods:

public GeoPointND newPoint(int dimension, Construction cons) { return new GeoPoint(cons); } public GeoVectorND newVector(int dimension, Construction cons) { return new GeoVector(cons); }
登入後複製

It looks a bit strange. Thedimensionargument isn't used at all. What's going on here? Inheritance, of course! Let's find theGeoFactory3Dinheritor class and see what happens in it.

@Override public GeoVectorND newVector(int dimension, Construction cons) { if (dimension == 3) { return new GeoVector3D(cons); } return new GeoVector(cons); } @Override public GeoPointND newPoint(int dimension, Construction cons) { return dimension == 3 ? new GeoPoint3D(cons) : new GeoPoint(cons); }
登入後複製

Excellent! Now we can admire the creation of four different objects in all their glory. We return to the code fragment with the possible error. ForPOINTandPOINT3D, the objects of theGeoPointandGeoPoint3Dclasses are created.GeoVectoris created forVECTOR, but poorGeoVector3Dseems to have been abandoned.

Sure, it's a bit strange to use a factory method pattern in two cases and call constructors directly in the remaining two. Is this a leftover from the refactoring process, or is it some kind of temporary solution that'll stick around until the end of time? In my opinion, if the responsibility for creating objects has been handed over to another class, it'd be better to fully delegate that responsibility.

Missing quadrillions

@Override final public void update() { .... switch (angle.getAngleStyle()) { .... case EuclidianStyleConstants.RIGHT_ANGLE_STYLE_L: // Belgian offset |_ if (square == null) { square = AwtFactory.getPrototype().newGeneralPath(); } else { square.reset(); } length = arcSize * 0.7071067811865; // <= double offset = length * 0.4; square.moveTo( coords[0] + length * Math.cos(angSt) + offset * Math.cos(angSt) + offset * Math.cos(angSt + Kernel.PI_HALF), coords[1] - length * Math.sin(angSt) * view.getScaleRatio() - offset * Math.sin(angSt) - offset * Math.sin(angSt + Kernel.PI_HALF)); .... break; } }
登入後複製

Where's the warning? There it is:

V6107 The constant 0.7071067811865 is being utilized. The resulting value could be inaccurate. Consider using Math.sqrt(0.5). DrawAngle.java 303

このコード断片のインスタンスが 4 つ見つかりました。より正確な値は 0.7071067811865476 です。チャレンジして暗記してみてください、ふふ。最後の 3 桁は省略されます。クリティカルですか?おそらく精度は十分ですが、この場合のように、事前定義された定数またはMath.sqrt(0.5)を使用すると、失われた桁を回復できるだけでなく、タイプミスのリスクも排除できます。コードの可読性がどのように向上するかに注目してください。おそらく、0.707 を見ると、それがどのような魔法の数字であるかをすぐに理解できる人がいるでしょう。ただし、コードの可読性を高めるためだけに、魔法の効果を少し減らすことができる場合もあります。

結論

私たちの小さな数学の旅は終わりました。ご覧のとおり、多くの幾何学的な課題に取り組んだ後でも、コードで単純な間違いを犯す可能性があります。コードが新鮮で、開発者が過去の意図をまだ念頭に置いておくことができれば素晴らしいことです。このような場合、人々は必要な変更があれば理解するかもしれませんが、時間が経つと、これらの問題を修正する必要性を認識するのはそれほど簡単ではないかもしれません。

つまり、アナライザーは、私のように一度実行するのではなく、コードを書いている時点で非常に効率的であることがわかります。特に今では、PVS-Studio アナライザーのようなツールが自由に使えるようになりました。

同様のトピックに関する他の記事

  1. 数学ソフトウェアの使用による頭痛
  2. 数学者: 信頼しますが、検証してください。
  3. 大きな電卓がおかしくなりました
  4. トランスプロテオミクスパイプライン (TPP) プロジェクトの分析
  5. COVID-19 の研究と初期化されていない変数。
  6. 科学的データ分析フレームワークであるROOTのコードを分析しています
  7. NCBI ゲノム ワークベンチ: 脅威にさらされている科学研究

以上是即使是偉大的數學家也會犯錯的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:dev.to
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!